@teamscala/service-boot 0.23.479 → 0.23.492

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.
Files changed (46) hide show
  1. package/package.json +1 -261
  2. package/src/agreement-gate/signing-session.ts +1 -1
  3. package/src/build-client-assets/styles-build.ts +11 -1
  4. package/src/mount-orpc-router.ts +1 -1
  5. package/src/register-page-server-client.ts +1 -15
  6. package/src/register-registry-page-route/region-slots-page-data.ts +2 -2
  7. package/src/register-ws-routes.ts +1 -1
  8. package/src/fn-routers/agreement-certificate.ts +0 -224
  9. package/src/fn-routers/agreement-completion-email.ts +0 -114
  10. package/src/fn-routers/agreement-send-router.ts +0 -303
  11. package/src/fn-routers/agreement-sign-side-effects.ts +0 -192
  12. package/src/fn-routers/agreement-signing-evidence.ts +0 -105
  13. package/src/fn-routers/assessment-report-data.test.ts +0 -404
  14. package/src/fn-routers/assessment-report-data.ts +0 -485
  15. package/src/fn-routers/assessment-report-email.ts +0 -256
  16. package/src/fn-routers/assessment-report-pair.test.ts +0 -107
  17. package/src/fn-routers/assessment-report-pair.ts +0 -155
  18. package/src/fn-routers/assessment-research-report-data.test.ts +0 -262
  19. package/src/fn-routers/assessment-research-report-data.ts +0 -424
  20. package/src/fn-routers/assessment-scoring.ts +0 -105
  21. package/src/fn-routers/auth-router.ts +0 -65
  22. package/src/fn-routers/chat-session-router-shared.ts +0 -122
  23. package/src/fn-routers/chat-session-router.ts +0 -356
  24. package/src/fn-routers/conversation-ownership.ts +0 -24
  25. package/src/fn-routers/email-defaults.ts +0 -28
  26. package/src/fn-routers/email-link-ttl.ts +0 -28
  27. package/src/fn-routers/index.ts +0 -76
  28. package/src/fn-routers/linkedin-identity.test.ts +0 -27
  29. package/src/fn-routers/linkedin-identity.ts +0 -36
  30. package/src/fn-routers/messaging-whatsapp-router.test.ts +0 -121
  31. package/src/fn-routers/messaging-whatsapp-router.ts +0 -200
  32. package/src/fn-routers/panel-data-router.ts +0 -104
  33. package/src/fn-routers/persist-chat-messages.test.ts +0 -86
  34. package/src/fn-routers/persist-chat-messages.ts +0 -98
  35. package/src/fn-routers/proposal-send-router.ts +0 -207
  36. package/src/fn-routers/public-agreement-router.ts +0 -407
  37. package/src/fn-routers/public-assessment-router.test.ts +0 -437
  38. package/src/fn-routers/public-assessment-router.ts +0 -268
  39. package/src/fn-routers/public-gpt-session-router.ts +0 -378
  40. package/src/fn-routers/resend-client.ts +0 -39
  41. package/src/fn-routers/sign-in-flow-router.ts +0 -196
  42. package/src/fn-routers/user-preferences-router.ts +0 -130
  43. package/src/fn-routers/work-items-router.ts +0 -342
  44. package/src/page-server-orpc/audit-revert-router.ts +0 -102
  45. package/src/page-server-orpc/puck-pages-router.ts +0 -170
  46. package/src/page-server-orpc/user-preferences-router.ts +0 -132
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.479",
9
+ "version": "0.23.492",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -380,242 +380,6 @@
380
380
  "types": "./src/expand-roster.d.ts",
381
381
  "default": "./src/expand-roster.ts"
382
382
  },
383
- "./fn-routers/agreement-certificate": {
384
- "types": "./src/fn-routers/agreement-certificate.d.ts",
385
- "default": "./src/fn-routers/agreement-certificate.ts"
386
- },
387
- "./fn-routers/agreement-certificate.ts": {
388
- "types": "./src/fn-routers/agreement-certificate.d.ts",
389
- "default": "./src/fn-routers/agreement-certificate.ts"
390
- },
391
- "./fn-routers/agreement-completion-email": {
392
- "types": "./src/fn-routers/agreement-completion-email.d.ts",
393
- "default": "./src/fn-routers/agreement-completion-email.ts"
394
- },
395
- "./fn-routers/agreement-completion-email.ts": {
396
- "types": "./src/fn-routers/agreement-completion-email.d.ts",
397
- "default": "./src/fn-routers/agreement-completion-email.ts"
398
- },
399
- "./fn-routers/agreement-send-router": {
400
- "types": "./src/fn-routers/agreement-send-router.d.ts",
401
- "default": "./src/fn-routers/agreement-send-router.ts"
402
- },
403
- "./fn-routers/agreement-send-router.ts": {
404
- "types": "./src/fn-routers/agreement-send-router.d.ts",
405
- "default": "./src/fn-routers/agreement-send-router.ts"
406
- },
407
- "./fn-routers/agreement-sign-side-effects": {
408
- "types": "./src/fn-routers/agreement-sign-side-effects.d.ts",
409
- "default": "./src/fn-routers/agreement-sign-side-effects.ts"
410
- },
411
- "./fn-routers/agreement-sign-side-effects.ts": {
412
- "types": "./src/fn-routers/agreement-sign-side-effects.d.ts",
413
- "default": "./src/fn-routers/agreement-sign-side-effects.ts"
414
- },
415
- "./fn-routers/agreement-signing-evidence": {
416
- "types": "./src/fn-routers/agreement-signing-evidence.d.ts",
417
- "default": "./src/fn-routers/agreement-signing-evidence.ts"
418
- },
419
- "./fn-routers/agreement-signing-evidence.ts": {
420
- "types": "./src/fn-routers/agreement-signing-evidence.d.ts",
421
- "default": "./src/fn-routers/agreement-signing-evidence.ts"
422
- },
423
- "./fn-routers/assessment-report-data": {
424
- "types": "./src/fn-routers/assessment-report-data.d.ts",
425
- "default": "./src/fn-routers/assessment-report-data.ts"
426
- },
427
- "./fn-routers/assessment-report-data.ts": {
428
- "types": "./src/fn-routers/assessment-report-data.d.ts",
429
- "default": "./src/fn-routers/assessment-report-data.ts"
430
- },
431
- "./fn-routers/assessment-report-email": {
432
- "types": "./src/fn-routers/assessment-report-email.d.ts",
433
- "default": "./src/fn-routers/assessment-report-email.ts"
434
- },
435
- "./fn-routers/assessment-report-email.ts": {
436
- "types": "./src/fn-routers/assessment-report-email.d.ts",
437
- "default": "./src/fn-routers/assessment-report-email.ts"
438
- },
439
- "./fn-routers/assessment-report-pair": {
440
- "types": "./src/fn-routers/assessment-report-pair.d.ts",
441
- "default": "./src/fn-routers/assessment-report-pair.ts"
442
- },
443
- "./fn-routers/assessment-report-pair.ts": {
444
- "types": "./src/fn-routers/assessment-report-pair.d.ts",
445
- "default": "./src/fn-routers/assessment-report-pair.ts"
446
- },
447
- "./fn-routers/assessment-research-report-data": {
448
- "types": "./src/fn-routers/assessment-research-report-data.d.ts",
449
- "default": "./src/fn-routers/assessment-research-report-data.ts"
450
- },
451
- "./fn-routers/assessment-research-report-data.ts": {
452
- "types": "./src/fn-routers/assessment-research-report-data.d.ts",
453
- "default": "./src/fn-routers/assessment-research-report-data.ts"
454
- },
455
- "./fn-routers/assessment-scoring": {
456
- "types": "./src/fn-routers/assessment-scoring.d.ts",
457
- "default": "./src/fn-routers/assessment-scoring.ts"
458
- },
459
- "./fn-routers/assessment-scoring.ts": {
460
- "types": "./src/fn-routers/assessment-scoring.d.ts",
461
- "default": "./src/fn-routers/assessment-scoring.ts"
462
- },
463
- "./fn-routers/auth-router": {
464
- "types": "./src/fn-routers/auth-router.d.ts",
465
- "default": "./src/fn-routers/auth-router.ts"
466
- },
467
- "./fn-routers/auth-router.ts": {
468
- "types": "./src/fn-routers/auth-router.d.ts",
469
- "default": "./src/fn-routers/auth-router.ts"
470
- },
471
- "./fn-routers/chat-session-router": {
472
- "types": "./src/fn-routers/chat-session-router.d.ts",
473
- "default": "./src/fn-routers/chat-session-router.ts"
474
- },
475
- "./fn-routers/chat-session-router.ts": {
476
- "types": "./src/fn-routers/chat-session-router.d.ts",
477
- "default": "./src/fn-routers/chat-session-router.ts"
478
- },
479
- "./fn-routers/chat-session-router-shared": {
480
- "types": "./src/fn-routers/chat-session-router-shared.d.ts",
481
- "default": "./src/fn-routers/chat-session-router-shared.ts"
482
- },
483
- "./fn-routers/chat-session-router-shared.ts": {
484
- "types": "./src/fn-routers/chat-session-router-shared.d.ts",
485
- "default": "./src/fn-routers/chat-session-router-shared.ts"
486
- },
487
- "./fn-routers/conversation-ownership": {
488
- "types": "./src/fn-routers/conversation-ownership.d.ts",
489
- "default": "./src/fn-routers/conversation-ownership.ts"
490
- },
491
- "./fn-routers/conversation-ownership.ts": {
492
- "types": "./src/fn-routers/conversation-ownership.d.ts",
493
- "default": "./src/fn-routers/conversation-ownership.ts"
494
- },
495
- "./fn-routers/email-defaults": {
496
- "types": "./src/fn-routers/email-defaults.d.ts",
497
- "default": "./src/fn-routers/email-defaults.ts"
498
- },
499
- "./fn-routers/email-defaults.ts": {
500
- "types": "./src/fn-routers/email-defaults.d.ts",
501
- "default": "./src/fn-routers/email-defaults.ts"
502
- },
503
- "./fn-routers/email-link-ttl": {
504
- "types": "./src/fn-routers/email-link-ttl.d.ts",
505
- "default": "./src/fn-routers/email-link-ttl.ts"
506
- },
507
- "./fn-routers/email-link-ttl.ts": {
508
- "types": "./src/fn-routers/email-link-ttl.d.ts",
509
- "default": "./src/fn-routers/email-link-ttl.ts"
510
- },
511
- "./fn-routers/index": {
512
- "types": "./src/fn-routers/index.d.ts",
513
- "default": "./src/fn-routers/index.ts"
514
- },
515
- "./fn-routers/index.ts": {
516
- "types": "./src/fn-routers/index.d.ts",
517
- "default": "./src/fn-routers/index.ts"
518
- },
519
- "./fn-routers": {
520
- "types": "./src/fn-routers/index.d.ts",
521
- "default": "./src/fn-routers/index.ts"
522
- },
523
- "./fn-routers/linkedin-identity": {
524
- "types": "./src/fn-routers/linkedin-identity.d.ts",
525
- "default": "./src/fn-routers/linkedin-identity.ts"
526
- },
527
- "./fn-routers/linkedin-identity.ts": {
528
- "types": "./src/fn-routers/linkedin-identity.d.ts",
529
- "default": "./src/fn-routers/linkedin-identity.ts"
530
- },
531
- "./fn-routers/messaging-whatsapp-router": {
532
- "types": "./src/fn-routers/messaging-whatsapp-router.d.ts",
533
- "default": "./src/fn-routers/messaging-whatsapp-router.ts"
534
- },
535
- "./fn-routers/messaging-whatsapp-router.ts": {
536
- "types": "./src/fn-routers/messaging-whatsapp-router.d.ts",
537
- "default": "./src/fn-routers/messaging-whatsapp-router.ts"
538
- },
539
- "./fn-routers/panel-data-router": {
540
- "types": "./src/fn-routers/panel-data-router.d.ts",
541
- "default": "./src/fn-routers/panel-data-router.ts"
542
- },
543
- "./fn-routers/panel-data-router.ts": {
544
- "types": "./src/fn-routers/panel-data-router.d.ts",
545
- "default": "./src/fn-routers/panel-data-router.ts"
546
- },
547
- "./fn-routers/persist-chat-messages": {
548
- "types": "./src/fn-routers/persist-chat-messages.d.ts",
549
- "default": "./src/fn-routers/persist-chat-messages.ts"
550
- },
551
- "./fn-routers/persist-chat-messages.ts": {
552
- "types": "./src/fn-routers/persist-chat-messages.d.ts",
553
- "default": "./src/fn-routers/persist-chat-messages.ts"
554
- },
555
- "./fn-routers/proposal-send-router": {
556
- "types": "./src/fn-routers/proposal-send-router.d.ts",
557
- "default": "./src/fn-routers/proposal-send-router.ts"
558
- },
559
- "./fn-routers/proposal-send-router.ts": {
560
- "types": "./src/fn-routers/proposal-send-router.d.ts",
561
- "default": "./src/fn-routers/proposal-send-router.ts"
562
- },
563
- "./fn-routers/public-agreement-router": {
564
- "types": "./src/fn-routers/public-agreement-router.d.ts",
565
- "default": "./src/fn-routers/public-agreement-router.ts"
566
- },
567
- "./fn-routers/public-agreement-router.ts": {
568
- "types": "./src/fn-routers/public-agreement-router.d.ts",
569
- "default": "./src/fn-routers/public-agreement-router.ts"
570
- },
571
- "./fn-routers/public-assessment-router": {
572
- "types": "./src/fn-routers/public-assessment-router.d.ts",
573
- "default": "./src/fn-routers/public-assessment-router.ts"
574
- },
575
- "./fn-routers/public-assessment-router.ts": {
576
- "types": "./src/fn-routers/public-assessment-router.d.ts",
577
- "default": "./src/fn-routers/public-assessment-router.ts"
578
- },
579
- "./fn-routers/public-gpt-session-router": {
580
- "types": "./src/fn-routers/public-gpt-session-router.d.ts",
581
- "default": "./src/fn-routers/public-gpt-session-router.ts"
582
- },
583
- "./fn-routers/public-gpt-session-router.ts": {
584
- "types": "./src/fn-routers/public-gpt-session-router.d.ts",
585
- "default": "./src/fn-routers/public-gpt-session-router.ts"
586
- },
587
- "./fn-routers/resend-client": {
588
- "types": "./src/fn-routers/resend-client.d.ts",
589
- "default": "./src/fn-routers/resend-client.ts"
590
- },
591
- "./fn-routers/resend-client.ts": {
592
- "types": "./src/fn-routers/resend-client.d.ts",
593
- "default": "./src/fn-routers/resend-client.ts"
594
- },
595
- "./fn-routers/sign-in-flow-router": {
596
- "types": "./src/fn-routers/sign-in-flow-router.d.ts",
597
- "default": "./src/fn-routers/sign-in-flow-router.ts"
598
- },
599
- "./fn-routers/sign-in-flow-router.ts": {
600
- "types": "./src/fn-routers/sign-in-flow-router.d.ts",
601
- "default": "./src/fn-routers/sign-in-flow-router.ts"
602
- },
603
- "./fn-routers/user-preferences-router": {
604
- "types": "./src/fn-routers/user-preferences-router.d.ts",
605
- "default": "./src/fn-routers/user-preferences-router.ts"
606
- },
607
- "./fn-routers/user-preferences-router.ts": {
608
- "types": "./src/fn-routers/user-preferences-router.d.ts",
609
- "default": "./src/fn-routers/user-preferences-router.ts"
610
- },
611
- "./fn-routers/work-items-router": {
612
- "types": "./src/fn-routers/work-items-router.d.ts",
613
- "default": "./src/fn-routers/work-items-router.ts"
614
- },
615
- "./fn-routers/work-items-router.ts": {
616
- "types": "./src/fn-routers/work-items-router.d.ts",
617
- "default": "./src/fn-routers/work-items-router.ts"
618
- },
619
383
  "./gate-ttls": {
620
384
  "types": "./src/gate-ttls.d.ts",
621
385
  "default": "./src/gate-ttls.ts"
@@ -888,14 +652,6 @@
888
652
  "types": "./src/page-route-shell-types.d.ts",
889
653
  "default": "./src/page-route-shell-types.ts"
890
654
  },
891
- "./page-server-orpc/audit-revert-router": {
892
- "types": "./src/page-server-orpc/audit-revert-router.d.ts",
893
- "default": "./src/page-server-orpc/audit-revert-router.ts"
894
- },
895
- "./page-server-orpc/audit-revert-router.ts": {
896
- "types": "./src/page-server-orpc/audit-revert-router.d.ts",
897
- "default": "./src/page-server-orpc/audit-revert-router.ts"
898
- },
899
655
  "./page-server-orpc/hub-mcp-client": {
900
656
  "types": "./src/page-server-orpc/hub-mcp-client.d.ts",
901
657
  "default": "./src/page-server-orpc/hub-mcp-client.ts"
@@ -904,14 +660,6 @@
904
660
  "types": "./src/page-server-orpc/hub-mcp-client.d.ts",
905
661
  "default": "./src/page-server-orpc/hub-mcp-client.ts"
906
662
  },
907
- "./page-server-orpc/puck-pages-router": {
908
- "types": "./src/page-server-orpc/puck-pages-router.d.ts",
909
- "default": "./src/page-server-orpc/puck-pages-router.ts"
910
- },
911
- "./page-server-orpc/puck-pages-router.ts": {
912
- "types": "./src/page-server-orpc/puck-pages-router.d.ts",
913
- "default": "./src/page-server-orpc/puck-pages-router.ts"
914
- },
915
663
  "./page-server-orpc/server-client": {
916
664
  "types": "./src/page-server-orpc/server-client.d.ts",
917
665
  "default": "./src/page-server-orpc/server-client.ts"
@@ -920,14 +668,6 @@
920
668
  "types": "./src/page-server-orpc/server-client.d.ts",
921
669
  "default": "./src/page-server-orpc/server-client.ts"
922
670
  },
923
- "./page-server-orpc/user-preferences-router": {
924
- "types": "./src/page-server-orpc/user-preferences-router.d.ts",
925
- "default": "./src/page-server-orpc/user-preferences-router.ts"
926
- },
927
- "./page-server-orpc/user-preferences-router.ts": {
928
- "types": "./src/page-server-orpc/user-preferences-router.d.ts",
929
- "default": "./src/page-server-orpc/user-preferences-router.ts"
930
- },
931
671
  "./pg-boss-handler-registry": {
932
672
  "types": "./src/pg-boss-handler-registry.d.ts",
933
673
  "default": "./src/pg-boss-handler-registry.ts"
@@ -20,7 +20,7 @@ import {
20
20
  import {
21
21
  buildCertificateData,
22
22
  type CertificateData,
23
- } from "../fn-routers/agreement-certificate.ts";
23
+ } from "@teamscala/agreement-generation/agreement-certificate";
24
24
  import { findFirstAgreement } from "./agreement-lookup.ts";
25
25
  import { resolveAgreementViewerDirectory } from "./resolution-caches.ts";
26
26
 
@@ -213,8 +213,18 @@ export async function buildTailwindCss(
213
213
  await mkdir(outDir, { recursive: true });
214
214
 
215
215
  const cssSource = await Bun.file(entry).text();
216
+ // base MUST be the ENTRY FILE's directory (src/), never the service root.
217
+ // The generated app.css authors its @source dirs relative to itself
218
+ // ("../node_modules/@teamscala/<pkg>/src"); Tailwind 4.3 resolves @source
219
+ // against THIS base, so passing the service root aimed every scan one
220
+ // level too high (workspace/node_modules — empty) and the candidate set
221
+ // collapsed to zero: build([]) strips every Tailwind utility and every
222
+ // kind:ui service renders with naked buttons/controls while the
223
+ // stylesheet still loads with its full package-rule payload (measured
224
+ // 2026-09-05, operator-visible fleet-wide). Package-specifier @imports are
225
+ // unaffected — they resolve through loadStylesheet's Bun.resolve walk.
216
226
  const compiler = await compile(cssSource, {
217
- base: cwd,
227
+ base: join(cwd, "src"),
218
228
  async loadStylesheet(id, base) {
219
229
  return bunResolveStylesheet(id, base, cwd);
220
230
  },
@@ -6,7 +6,7 @@
6
6
  * Mounts the composed ORPC app router (generated Prisma CRUD + custom fn routers)
7
7
  * at /api/rpc/* over HTTP via @orpc/server/fetch's RPCHandler.
8
8
  * Uses getAppRouter() from @teamscala/orpc/app-router which merges generated
9
- * routers with custom routers registered via registerCustomRouter().
9
+ * routers plus the row-composed fn namespace (orpc-bundle from api_route rows).
10
10
  */
11
11
 
12
12
  import { join } from "node:path";
@@ -99,21 +99,7 @@ export async function registerPageServerClient(
99
99
  );
100
100
  }
101
101
 
102
- const { puckPagesRouter } = await import(
103
- "@teamscala/service-boot/page-server-orpc/puck-pages-router"
104
- );
105
- const { userPreferencesRouter } = await import(
106
- "@teamscala/service-boot/page-server-orpc/user-preferences-router"
107
- );
108
- const { auditRevertRouter } = await import(
109
- "@teamscala/service-boot/page-server-orpc/audit-revert-router"
110
- );
111
- const composed = {
112
- ...((appRouter as Record<string, unknown> | null) ?? {}),
113
- puckPages: puckPagesRouter,
114
- userPreferences: userPreferencesRouter,
115
- auditRevert: auditRevertRouter,
116
- };
102
+ const composed = (appRouter as Record<string, unknown> | null) ?? {};
117
103
  const { loadRegistryConfig } = await import(
118
104
  "@teamscala/db/registry/load-config"
119
105
  );
@@ -92,7 +92,7 @@ export function regionSlotsPageData(
92
92
  {
93
93
  templateTitle: (meta.title as string) || undefined,
94
94
  websiteName: (website.name as string) || undefined,
95
- organisationName: (brand.companyName as string) || undefined,
95
+ organisationName: (brand.companyFriendlyName as string) || (brand.companyName as string) || undefined,
96
96
  entityTitle: entityTitle || undefined,
97
97
  },
98
98
  { orgPrefix },
@@ -164,7 +164,7 @@ export function regionSlotsPageData(
164
164
  {
165
165
  templateTitle: (meta.title as string) || undefined,
166
166
  websiteName: (website.name as string) || undefined,
167
- organisationName: (brand.companyName as string) || undefined,
167
+ organisationName: (brand.companyFriendlyName as string) || (brand.companyName as string) || undefined,
168
168
  pageName: contentPageName,
169
169
  entityTitle: entityTitle || undefined,
170
170
  },
@@ -6,7 +6,7 @@
6
6
  * Registers each `service_install.websocket_routes[]` entry as a Hono WS route.
7
7
  * The ORPC WS (`/api/rpc/ws`) is mounted by the canonical primitive
8
8
  * `mountOrpcWs` (@teamscala/orpc/ws/server) over the FULLY-COMPOSED app router —
9
- * this runs AFTER registerFnRouters, so the eager RPCHandler snapshots every
9
+ * this runs AFTER the app router composition, so the eager RPCHandler snapshots every
10
10
  * namespace, and any `output: eventIterator(...)` procedure dispatches (rule
11
11
  * `orpc-ws-via-mount-orpc-ws`). It is NOT a server_package WsRouteHandler — it
12
12
  * is exempt from the legacy invariant below.
@@ -1,224 +0,0 @@
1
- /**
2
- * @system service-boot
3
- * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The Certificate of Completion for an executed agreement: DB-driven display
7
- * copy (config/certificate-of-completion), the CertificateData shape, and
8
- * the party-gated builder.
9
- *
10
- * Split out of fn-routers/public-agreement-router.ts (853 lines) —
11
- * byte-for-byte move; import paths adjusted only.
12
- */
13
-
14
- import * as v from "valibot";
15
- import { queryRows } from "@teamscala/db/query-rows";
16
- import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
17
- import {
18
- parseAgreementParties as parseParties,
19
- } from "@teamscala/document-signing/agreement-parties";
20
- import { parseEvidence } from "./agreement-signing-evidence.ts";
21
-
22
- /** The Certificate of Completion display copy. DB-driven via the
23
- * `config/certificate-of-completion` registry row — mirrors the agreement gate's
24
- * `config/content-gate-agreement` pattern (same signing surface), so an operator
25
- * can rebrand/reword the certificate via `registry_edit` with no deploy; the
26
- * hardcoded defaults are the fallback. Generic structural data-column labels
27
- * (Signer/Role/Envelope ID/…) stay in the component, exactly as the gate keeps
28
- * its structural markup in code. */
29
- export interface CertificateCopy {
30
- title: string;
31
- signersHeading: string;
32
- sealHeading: string;
33
- sealNote: string;
34
- footer: string;
35
- badgeCompleted: string;
36
- badgeInProgress: string;
37
- /** Audit-table column labels — DB-driven so rewording is a registry_edit.
38
- * Each labels BOTH the <th> header AND the stacked-row data-label on mobile
39
- * (one source, kept in sync), never a second hardcoded copy. */
40
- colSigner: string;
41
- colRole: string;
42
- colSigned: string;
43
- colIp: string;
44
- colAuth: string;
45
- }
46
-
47
- const CERTIFICATE_COPY_DEFAULTS: CertificateCopy = {
48
- title: "CERTIFICATE OF COMPLETION",
49
- signersHeading: "Signers",
50
- sealHeading: "Tamper-evident seal",
51
- sealNote:
52
- "{{hashAlgo}} hash of the executed document + signed roster, sealed {{hashedAt}}. Re-hashing the stored document and comparing to this value proves it has not been altered since signing.",
53
- footer:
54
- "This certificate is a record of the electronic signing of the above agreement, produced from the platform's permanent audit trail. Electronic signatures are legally recognised under the ESIGN Act and UETA (US) and eIDAS (EU).",
55
- badgeCompleted: "Completed",
56
- badgeInProgress: "In progress",
57
- colSigner: "Signer",
58
- colRole: "Role",
59
- colSigned: "Signed (UTC)",
60
- colIp: "IP address",
61
- colAuth: "Auth",
62
- };
63
-
64
- const CertificateCopySchema = v.object({
65
- title: v.optional(v.string(), CERTIFICATE_COPY_DEFAULTS.title),
66
- signersHeading: v.optional(
67
- v.string(),
68
- CERTIFICATE_COPY_DEFAULTS.signersHeading,
69
- ),
70
- sealHeading: v.optional(v.string(), CERTIFICATE_COPY_DEFAULTS.sealHeading),
71
- sealNote: v.optional(v.string(), CERTIFICATE_COPY_DEFAULTS.sealNote),
72
- footer: v.optional(v.string(), CERTIFICATE_COPY_DEFAULTS.footer),
73
- badgeCompleted: v.optional(
74
- v.string(),
75
- CERTIFICATE_COPY_DEFAULTS.badgeCompleted,
76
- ),
77
- badgeInProgress: v.optional(
78
- v.string(),
79
- CERTIFICATE_COPY_DEFAULTS.badgeInProgress,
80
- ),
81
- colSigner: v.optional(v.string(), CERTIFICATE_COPY_DEFAULTS.colSigner),
82
- colRole: v.optional(v.string(), CERTIFICATE_COPY_DEFAULTS.colRole),
83
- colSigned: v.optional(v.string(), CERTIFICATE_COPY_DEFAULTS.colSigned),
84
- colIp: v.optional(v.string(), CERTIFICATE_COPY_DEFAULTS.colIp),
85
- colAuth: v.optional(v.string(), CERTIFICATE_COPY_DEFAULTS.colAuth),
86
- });
87
-
88
- async function loadCertificateCopy(): Promise<CertificateCopy | null> {
89
- try {
90
- return await loadRegistryConfig(
91
- "config",
92
- "certificate-of-completion",
93
- CertificateCopySchema,
94
- );
95
- } catch {
96
- // Absent signal, not a masking fallback (protective-layers-fail-explicitly):
97
- // null propagates to CertificateData.copy, and the component renders its
98
- // FALLBACK_COPY — same default values, but the absence is honest rather
99
- // than disguised as a successful config load.
100
- return null;
101
- }
102
- }
103
-
104
- /** The Certificate of Completion data shape — the court-style audit certificate.
105
- * One home: {@link buildCertificateData} is consumed by the SSR agreement
106
- * gate, so the signing block receives the server-seeded prop. */
107
- export interface CertificateData {
108
- ok: boolean;
109
- envelopeId?: string;
110
- documentName?: string | null;
111
- issuedAt?: string;
112
- complete?: boolean;
113
- createdAt?: string | null;
114
- sentAt?: string | null;
115
- viewedAt?: string | null;
116
- signedAt?: string | null;
117
- documentHash?: string | null;
118
- hashAlgo?: string | null;
119
- hashedAt?: string | null;
120
- parties?: {
121
- role: string;
122
- name: string | null;
123
- email: string | null;
124
- title: string | null;
125
- company: string | null;
126
- signedAt: string | null;
127
- ip: string | null;
128
- userAgent: string | null;
129
- authMethod: string | null;
130
- }[];
131
- /** DB-driven display copy (config/certificate-of-completion); null when the
132
- * config row is missing/broken — the component falls back to FALLBACK_COPY. */
133
- copy?: CertificateCopy | null;
134
- }
135
-
136
- /**
137
- * Build the Certificate of Completion data for an agreement, gated to a party
138
- * email — signer evidence (IP/device) is never exposed to a non-party. One data
139
- * home: the SSR agreement gate (`buildSigningSession` in agreement-gate.ts)
140
- * calls this. The read is
141
- * intentionally RLS-unscoped via the raw registry prisma — the certificate is
142
- * reached without a session (AG cookie only), which context.prisma's RLS would
143
- * otherwise treat as no-org and hide. Accepts either the agreement record id or
144
- * its page id (the UUID the signer sees in the URL is the page id).
145
- */
146
- export async function buildCertificateData(
147
- agreementId: string,
148
- email: string,
149
- ): Promise<CertificateData> {
150
- const id = agreementId.trim();
151
- const mail = email.trim().toLowerCase();
152
- if (!id || !mail) return { ok: false };
153
- const [agreement] = await queryRows<
154
- {
155
- id: string;
156
- name: string;
157
- created_at: Date | null;
158
- sent_at: Date | null;
159
- viewed_at: Date | null;
160
- signed_at: Date | null;
161
- signature_data: unknown;
162
- signer_email: string | null;
163
- signer_name: string | null;
164
- }
165
- >`
166
- SELECT id, name, created_at, sent_at, viewed_at, signed_at,
167
- signature_data, signer_email, signer_name
168
- FROM agreements
169
- WHERE (id = ${id} OR page_id = ${id})
170
- LIMIT 1`;
171
- if (!agreement) return { ok: false };
172
- const parties = parseParties(agreement.signature_data);
173
- const evidence = parseEvidence(agreement.signature_data);
174
- const isParty = parties
175
- ? parties.some((p) => p.email.trim().toLowerCase() === mail)
176
- : String(agreement.signer_email ?? "")
177
- .trim()
178
- .toLowerCase() === mail;
179
- if (!isParty) return { ok: false };
180
- const complete =
181
- !!agreement.signed_at || (parties?.every((p) => p.signedAt) ?? false);
182
- const iso = (d: Date | null | undefined) => d?.toISOString() ?? null;
183
- const copy = await loadCertificateCopy();
184
- return {
185
- ok: true,
186
- copy,
187
- envelopeId: agreement.id,
188
- documentName: agreement.name,
189
- issuedAt: new Date().toISOString(),
190
- complete,
191
- createdAt: iso(agreement.created_at),
192
- sentAt: iso(agreement.sent_at),
193
- viewedAt: iso(agreement.viewed_at),
194
- signedAt: iso(agreement.signed_at),
195
- documentHash: evidence?.documentHash ?? null,
196
- hashAlgo: evidence?.hashAlgo ?? null,
197
- hashedAt: evidence?.hashedAt ?? null,
198
- parties: parties
199
- ? parties.map((p) => ({
200
- role: p.key,
201
- name: p.name,
202
- email: p.email,
203
- title: p.title,
204
- company: p.company,
205
- signedAt: p.signedAt,
206
- ip: p.signedIp ?? null,
207
- userAgent: p.signedUserAgent ?? null,
208
- authMethod: p.authMethod ?? null,
209
- }))
210
- : [
211
- {
212
- role: "signer",
213
- name: agreement.signer_name,
214
- email: agreement.signer_email,
215
- title: null,
216
- company: null,
217
- signedAt: iso(agreement.signed_at),
218
- ip: null,
219
- userAgent: null,
220
- authMethod: "email-match",
221
- },
222
- ],
223
- };
224
- }