@certysign/sdk 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * client.signers — the people you sign on behalf of.
3
+ *
4
+ * Signing in someone's name needs a certificate that belongs to THEM, not to your
5
+ * organisation. This resource covers getting them one and checking they have it:
6
+ *
7
+ * signingStatus() can this person sign yet, and with what
8
+ * sponsor() buy them a work certificate, funded from your wallet
9
+ * resendSetup() re-send the account-setup link if they lost it
10
+ *
11
+ * The boundary these keep: an API key can START a certificate but never FINISH one.
12
+ * The person proves their identity themselves, to a human reviewer. A key that could
13
+ * both invent a signer and obtain their certificate would produce signatures that
14
+ * attest to nothing.
15
+ */
16
+ class SignerResource {
17
+ constructor(http) {
18
+ this._http = http;
19
+ }
20
+
21
+ /**
22
+ * Can this person sign yet, and with which certificate?
23
+ *
24
+ * Worth calling BEFORE a batch: it turns a per-document failure — discovered
25
+ * halfway through, after the wallet has been charged — into one answer up front.
26
+ *
27
+ * Requires `cert:status`.
28
+ *
29
+ * @param {string} email
30
+ * @param {object} [opts]
31
+ * @param {boolean} [opts.issueCompletionUrl=false]
32
+ * For a signer stuck at `AWAITING_IDENTITY_VERIFICATION`, also return a URL you
33
+ * can redirect them to, so they need not hunt for the invitation email.
34
+ *
35
+ * This MINTS A NEW LINK and the previous one stops working — including the one
36
+ * already sitting in their inbox. Ask for it when you are about to redirect
37
+ * someone, not on every poll. Needs `signer:sponsor`, since minting an
38
+ * onboarding credential is the same class of act as sponsoring.
39
+ *
40
+ * @example
41
+ * const { data } = await client.signers.signingStatus('jane@example.com');
42
+ * if (!data.canSign) {
43
+ * // data.reason is NO_SIGNING_CERTIFICATE or AWAITING_IDENTITY_VERIFICATION
44
+ * }
45
+ *
46
+ * @example <caption>Redirect a signer who never finished verifying</caption>
47
+ * const { data } = await client.signers.signingStatus(email, { issueCompletionUrl: true });
48
+ * if (data.pendingSponsorship?.completionUrl) {
49
+ * return redirect(data.pendingSponsorship.completionUrl);
50
+ * }
51
+ */
52
+ signingStatus(email, { issueCompletionUrl = false } = {}) {
53
+ if (!email) throw new Error('signers.signingStatus: email is required');
54
+ const path = `/sdk/v1/signers/${encodeURIComponent(email)}/signing-status`;
55
+ return this._http.get(issueCompletionUrl ? `${path}?issueCompletionUrl=true` : path);
56
+ }
57
+
58
+ /**
59
+ * Sponsor a work certificate for a named person.
60
+ *
61
+ * Charges your wallet, creates the sponsorship and emails the signer. It also
62
+ * makes them a MEMBER of your organisation once the certificate issues — they get
63
+ * an account, appear in your member list, and their certificate falls under your
64
+ * governance.
65
+ *
66
+ * They cannot sign when this returns: `status` is `awaiting_member` until they
67
+ * complete identity verification. Redirect them to `data.completionUrl`, or let
68
+ * the invitation email carry them there.
69
+ *
70
+ * Requires `signer:sponsor`, a paid plan, and a funded wallet.
71
+ *
72
+ * @param {object} opts
73
+ * @param {string} opts.email
74
+ * @param {string} [opts.name] shown on the invitation; the certificate
75
+ * carries the name on their verified ID
76
+ * @param {'ses'|'ades'|'qes'} [opts.tier='ades']
77
+ * @param {boolean} [opts.restrictToSponsor=true] only signs your documents
78
+ *
79
+ * @example
80
+ * const { data } = await client.signers.sponsor({ email: 'jane@example.com', tier: 'ades' });
81
+ * return redirect(data.completionUrl);
82
+ */
83
+ sponsor({ email, name, tier = 'ades', restrictToSponsor = true } = {}) {
84
+ if (!email) throw new Error('signers.sponsor: email is required');
85
+ return this._http.post('/sdk/v1/signers/sponsor', {
86
+ data: { email, ...(name && { name }), tier, restrictToSponsor },
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Re-send the account-setup link to a signer you sponsored.
92
+ *
93
+ * The original link is returned once and stored hashed, so it cannot be looked up.
94
+ * This mints a FRESH one, which means any previous link stops working.
95
+ *
96
+ * Only works while the account is unclaimed: once the signer has chosen a
97
+ * password this returns 409 ALREADY_SET_UP, because handing a live account a new
98
+ * setup link would be a way to take it over. Rate limited to one send every five
99
+ * minutes per signer.
100
+ *
101
+ * Requires `signer:sponsor`.
102
+ *
103
+ * @example
104
+ * try {
105
+ * await client.signers.resendSetup('jane@example.com');
106
+ * } catch (e) {
107
+ * if (e.code === 'ALREADY_SET_UP') // she can sign in, or use forgot-password
108
+ * if (e.code === 'RESEND_TOO_SOON') // e.details.data.retryAfterSeconds
109
+ * }
110
+ */
111
+ resendSetup(email) {
112
+ if (!email) throw new Error('signers.resendSetup: email is required');
113
+ return this._http.post(
114
+ `/sdk/v1/signers/${encodeURIComponent(email)}/resend-setup`,
115
+ { data: {} },
116
+ );
117
+ }
118
+ }
119
+
120
+ module.exports = { SignerResource };
@@ -109,7 +109,7 @@ class SigningResource {
109
109
 
110
110
  const form = new FormData();
111
111
  for (const doc of documents) {
112
- _appendDocument(form, doc.data, doc.filename || 'document.pdf');
112
+ _appendDocument(form, doc.data, doc.filename || 'document.pdf', 'documents');
113
113
  }
114
114
  form.append('signerName', signerName);
115
115
  if (signerEmail) form.append('signerEmail', signerEmail);
@@ -157,12 +157,16 @@ class SigningResource {
157
157
 
158
158
  // ── Helpers ──────────────────────────────────────────────────────────────────
159
159
 
160
- function _appendDocument(form, document, filename) {
161
- if (Buffer.isBuffer(document)) {
162
- form.append('document', document, { filename, contentType: 'application/pdf' });
163
- } else if (document && typeof document.pipe === 'function') {
164
- // ReadStream
165
- form.append('document', document, { filename, contentType: 'application/pdf' });
160
+ /**
161
+ * @param {string} [field] multipart field name. The single-document endpoint reads
162
+ * `document` (multer `.single('document')`); the batch endpoint reads `documents`
163
+ * (multer `.array('documents')`). This was hard-coded to 'document' for both, so
164
+ * every batchSign call had its files rejected with LIMIT_UNEXPECTED_FILE before
165
+ * reaching the handler — the method could never have worked.
166
+ */
167
+ function _appendDocument(form, document, filename, field = 'document') {
168
+ if (Buffer.isBuffer(document) || (document && typeof document.pipe === 'function')) {
169
+ form.append(field, document, { filename, contentType: 'application/pdf' });
166
170
  } else {
167
171
  throw new Error(`Document must be a Buffer or ReadStream, got ${typeof document}`);
168
172
  }