@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
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.467",
9
+ "version": "0.23.469",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -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
- * Multi-party model: signature_data holds a parties roster (JSON); each party
15
- * is gated by its own email and the agreement completes only when every party
16
- * has signed; single-signer falls back to signer_email/signer_name.
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
- // Legacy single-signer path.
118
- const matched =
119
- String(agreement.signer_email ?? "")
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
- // Legacy single-signer path.
313
- const signerEmail = String(agreement.signer_email ?? "")
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: true as const,
350
- signedAt: updated.signed_at,
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
- log.error(
356
- `[publicAgreement.sign] failed: ${err instanceof Error ? err.message : String(err)}`,
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
  }),