@teamscala/service-boot 0.23.467 → 0.23.469
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/package.json
CHANGED
|
@@ -11,9 +11,19 @@
|
|
|
11
11
|
* Off-the-shelf path (building-applications): typed ORPC procedure, not a
|
|
12
12
|
* hand-written Hono handler; writes go through context.prisma inside the
|
|
13
13
|
* handler (orpc-is-the-only-api) so the audit trigger sees the ORPC caller.
|
|
14
|
-
*
|
|
15
|
-
* is gated by its own email and the
|
|
16
|
-
* has signed
|
|
14
|
+
* ONE signing model, the DocuSign recipients shape: signature_data holds a
|
|
15
|
+
* parties roster (JSON), each party is gated by its own email, and the
|
|
16
|
+
* agreement completes only when every party has signed. A single signer is a
|
|
17
|
+
* roster of one -- there is no separate single-signer path.
|
|
18
|
+
*
|
|
19
|
+
* A legacy signer_email/signer_name fallback used to sit alongside this and was
|
|
20
|
+
* deleted 2026-09-03: measured across every agreement on the platform, 13 of 14
|
|
21
|
+
* carried a roster and ZERO used the fallback (the 14th was an unsent draft with
|
|
22
|
+
* no signature_data at all). It was never exercised, and it was not inert -- its
|
|
23
|
+
* write put a BARE STRING into signature_data, which would have destroyed the
|
|
24
|
+
* roster of any agreement that reached it. Keeping it also meant every change to
|
|
25
|
+
* this handler had to be made twice: the Prisma 8 timestamptz fix that made
|
|
26
|
+
* signing work at all had to be applied to both branches of one function.
|
|
17
27
|
*
|
|
18
28
|
* This module holds the three procedures (verify/sign/verifyDocument); the
|
|
19
29
|
* supporting machinery lives as siblings (agreement-signing-evidence,
|
|
@@ -25,6 +35,7 @@ import { os } from "@orpc/server";
|
|
|
25
35
|
import * as v from "valibot";
|
|
26
36
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
27
37
|
import { queryRows } from "@teamscala/db/query-rows";
|
|
38
|
+
import { recordEvent } from "@teamscala/event-log/record-event";
|
|
28
39
|
import type { PrismaClient } from "@teamscala/db/client";
|
|
29
40
|
import { signAgreementAccessToken } from "@teamscala/document-signing/agreement-access-token";
|
|
30
41
|
import {
|
|
@@ -114,19 +125,9 @@ export const publicAgreementRouter = {
|
|
|
114
125
|
};
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.trim()
|
|
121
|
-
.toLowerCase() === email;
|
|
122
|
-
if (!matched) return { matched: false as const };
|
|
123
|
-
return {
|
|
124
|
-
matched: true,
|
|
125
|
-
name: agreement.name,
|
|
126
|
-
signerName: agreement.signer_name,
|
|
127
|
-
signedAt: agreement.signed_at,
|
|
128
|
-
accessToken: signAgreementAccessToken(agreementId, email, Date.now()),
|
|
129
|
-
};
|
|
128
|
+
// No roster means the agreement was never prepared for signing. It is
|
|
129
|
+
// not viewable, and it is certainly not signable.
|
|
130
|
+
return { matched: false as const };
|
|
130
131
|
}),
|
|
131
132
|
|
|
132
133
|
// Commit the authed party's signature. Returns { ok, parties } so the block
|
|
@@ -309,52 +310,39 @@ export const publicAgreementRouter = {
|
|
|
309
310
|
return result;
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
.trim()
|
|
315
|
-
.toLowerCase();
|
|
316
|
-
if (!signerEmail || signerEmail !== email) {
|
|
317
|
-
return {
|
|
318
|
-
ok: false as const,
|
|
319
|
-
error: "This email does not match the agreement's signer.",
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
if (agreement.signed_at) {
|
|
323
|
-
return {
|
|
324
|
-
ok: true as const,
|
|
325
|
-
alreadySigned: true,
|
|
326
|
-
signedAt: agreement.signed_at,
|
|
327
|
-
signerName: agreement.signer_name,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
const updated = await context.prisma.agreements.update({
|
|
331
|
-
where: { id: agreementId },
|
|
332
|
-
data: {
|
|
333
|
-
signature_data: input.signatureData ?? agreement.signer_name ?? "",
|
|
334
|
-
signed_at: nowInstant,
|
|
335
|
-
viewed_at: nowInstant,
|
|
336
|
-
signed_by: agreement.contact_id ?? null,
|
|
337
|
-
...(input.signerName ? { signer_name: input.signerName } : {}),
|
|
338
|
-
},
|
|
339
|
-
});
|
|
340
|
-
// Every captured identity → CRM: connect this signer to client_database.
|
|
341
|
-
await connectSignerToCrm(
|
|
342
|
-
context.prisma,
|
|
343
|
-
agreement,
|
|
344
|
-
email,
|
|
345
|
-
input.signerName ?? agreement.signer_name,
|
|
346
|
-
input.company,
|
|
347
|
-
);
|
|
313
|
+
// An agreement with no roster was never prepared for signing.
|
|
314
|
+
// Refuse rather than invent a signer.
|
|
348
315
|
return {
|
|
349
|
-
ok:
|
|
350
|
-
|
|
351
|
-
signerName: updated.signer_name,
|
|
352
|
-
payUrl: await invoicePayUrl(agreementId, originFrom(context.headers)),
|
|
316
|
+
ok: false as const,
|
|
317
|
+
error: "This agreement has no signers recorded and cannot be signed.",
|
|
353
318
|
};
|
|
354
319
|
} catch (err) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
320
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
321
|
+
log.error(`[publicAgreement.sign] failed: ${detail}`);
|
|
322
|
+
// A LOG LINE IS NOT A RECORD. Measured 2026-09-03: signing was broken
|
|
323
|
+
// for every signer for hours -- first a 404 on the procedure, then a
|
|
324
|
+
// Prisma 8 codec rejecting the timestamp write -- and error_logs held
|
|
325
|
+
// ZERO rows the whole time. The page returned 200, the service was
|
|
326
|
+
// running, the deploy was green, and the signer saw only "Could not
|
|
327
|
+
// submit your signature.", which reads as transient and invites a
|
|
328
|
+
// retry that fails identically. The failure was discovered by a human
|
|
329
|
+
// being unable to sign, which is the same shape as
|
|
330
|
+
// page-server.render.failed above and takes the same cure: emit a
|
|
331
|
+
// durable event so a surface can answer "is signing failing" without
|
|
332
|
+
// somebody attempting a signature.
|
|
333
|
+
//
|
|
334
|
+
// Fire-and-forget and last: telemetry may never change what the signer
|
|
335
|
+
// is served, and this path is already handling a failure
|
|
336
|
+
// (observability-is-passive). The agreement id and the signer role
|
|
337
|
+
// make a recorded event lead to the specific failure.
|
|
338
|
+
recordEvent({
|
|
339
|
+
kind: "agreement.sign.failed",
|
|
340
|
+
payload: {
|
|
341
|
+
agreementId,
|
|
342
|
+
error: detail.slice(0, 500),
|
|
343
|
+
errorName: err instanceof Error ? err.name : "unknown",
|
|
344
|
+
},
|
|
345
|
+
});
|
|
358
346
|
return { ok: false as const, error: "Could not submit your signature." };
|
|
359
347
|
}
|
|
360
348
|
}),
|