@teamscala/service-boot 0.23.466 → 0.23.467

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.466",
9
+ "version": "0.23.467",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -156,7 +156,29 @@ export const publicAgreementRouter = {
156
156
  });
157
157
  if (!agreement) return { ok: false as const, error: "Agreement not found" };
158
158
 
159
+ // TWO representations, deliberately, because they have two consumers.
160
+ //
161
+ // `now` is an ISO STRING and belongs in the parties roster, which is
162
+ // serialised to JSON in signature_data — a Temporal.Instant would
163
+ // stringify into that JSON as an object.
164
+ //
165
+ // `nowInstant` is what the timestamptz COLUMNS take. Under Prisma 8 the
166
+ // timestamptz codec ('pg/timestamptz-temporal@1') accepts a
167
+ // Temporal.Instant and NOTHING ELSE. Measured against the live service:
168
+ // a string gives "received a string", a Date gives "received a Date",
169
+ // and only Temporal.Instant writes. Passing `now` therefore threw on
170
+ // EVERY signature, and the handler's catch reported the generic "Could
171
+ // not submit your signature." -- so signing was impossible for every
172
+ // signer with nothing naming the cause.
173
+ //
174
+ // The cast is load-bearing and is NOT laziness: the generated Prisma
175
+ // types declare these fields `string | Date` and reject Instant, so the
176
+ // client's TYPES and its RUNTIME CODEC disagree. Following the types
177
+ // produces code that compiles and always throws, which is exactly how
178
+ // this shipped. The runtime is the authority here; the generated types
179
+ // are the defect, and they are the thing to fix upstream.
159
180
  const now = new Date().toISOString();
181
+ const nowInstant = Temporal.Now.instant() as unknown as Date;
160
182
 
161
183
  try {
162
184
  // Multi-party path: signature_data holds a parties roster.
@@ -229,10 +251,10 @@ export const publicAgreementRouter = {
229
251
  signature_data: JSON.stringify(
230
252
  evidence ? { parties, evidence } : { parties },
231
253
  ),
232
- viewed_at: now,
254
+ viewed_at: nowInstant,
233
255
  };
234
256
  if (executed) {
235
- data.signed_at = now;
257
+ data.signed_at = nowInstant;
236
258
  data.signed_by = agreement.contact_id ?? null;
237
259
  }
238
260
  const updated = await context.prisma.agreements.update({
@@ -309,8 +331,8 @@ export const publicAgreementRouter = {
309
331
  where: { id: agreementId },
310
332
  data: {
311
333
  signature_data: input.signatureData ?? agreement.signer_name ?? "",
312
- signed_at: now,
313
- viewed_at: now,
334
+ signed_at: nowInstant,
335
+ viewed_at: nowInstant,
314
336
  signed_by: agreement.contact_id ?? null,
315
337
  ...(input.signerName ? { signer_name: input.signerName } : {}),
316
338
  },
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * applySessionOrgBranding must never repaint a TENANT document in the
7
+ * VIEWER's brand.
8
+ *
9
+ * A public document belongs to the org that PUBLISHED it and must look
10
+ * identical to every reader. Personalising to the reader's org is right for a
11
+ * portal (you are looking at your own workspace) and wrong for a document.
12
+ *
13
+ * The regression this locks: sign.scala.business's website is owned by the
14
+ * PLATFORM org, so the website-level test passed and the session override
15
+ * fired on agreements owned by tenants. Every signed-in staff viewer saw a
16
+ * GMG APAC agreement in their own org's brand while an anonymous client saw
17
+ * it correctly — so the only people who could not see what the client sees
18
+ * were the people reviewing it before sending.
19
+ */
20
+
21
+ import { describe, expect, mock, test } from "bun:test";
22
+
23
+ const PLATFORM_ORG = "c503629a-85d9-4d25-9431-f6fa59504868";
24
+ const TENANT_ORG = "5b742740-0f37-494c-a059-d8399ae4b350";
25
+ const VIEWER_ORG = PLATFORM_ORG;
26
+
27
+ mock.module("@teamscala/db/registry/platform", () => ({
28
+ getPlatformOrgId: () => PLATFORM_ORG,
29
+ }));
30
+
31
+ // The override's only way to change anything is to fetch the viewer org's
32
+ // branding. If it is never called, no personalisation happened.
33
+ const fetchBranding = mock(async () => ({
34
+ company_name: "Viewer Org",
35
+ company_friendly_name: "Viewer Org",
36
+ brand_primary_color: "#000080",
37
+ brand_heading_font: null,
38
+ brand_body_font: null,
39
+ company_logo_for_light_theme: "/viewer-light.png",
40
+ company_logo_for_dark_theme: "/viewer-dark.png",
41
+ }));
42
+ mock.module("@teamscala/db/fetch-organisation-page-branding", () => ({
43
+ fetchOrganisationPageBranding: fetchBranding,
44
+ }));
45
+ mock.module("../page-server-orpc/server-client.ts", () => ({
46
+ getServerClient: () => ({}),
47
+ }));
48
+
49
+ const { applySessionOrgBranding } = await import("./apply-page-branding.ts");
50
+
51
+ function pageOwnedBy(pageOrgId: string, websiteOrgId: string) {
52
+ return {
53
+ orgId: pageOrgId,
54
+ website: { organisation_id: websiteOrgId },
55
+ brand: { companyName: "Publisher", primaryColor: "#FF4500" },
56
+ } as Record<string, unknown>;
57
+ }
58
+
59
+ describe("applySessionOrgBranding — a document is never repainted for its reader", () => {
60
+ test("a TENANT document on a PLATFORM-owned website keeps the publisher's brand", async () => {
61
+ fetchBranding.mockClear();
62
+ // Exactly the sign.scala.business shape: the website is the platform's,
63
+ // the agreement belongs to the tenant, the viewer is platform staff.
64
+ const pageData = pageOwnedBy(TENANT_ORG, PLATFORM_ORG);
65
+
66
+ await applySessionOrgBranding(pageData, undefined, VIEWER_ORG);
67
+
68
+ expect(fetchBranding).not.toHaveBeenCalled();
69
+ expect(pageData.orgId).toBe(TENANT_ORG);
70
+ expect((pageData.brand as { primaryColor: string }).primaryColor).toBe(
71
+ "#FF4500",
72
+ );
73
+ });
74
+
75
+ test("a TENANT-owned website is untouched too", async () => {
76
+ fetchBranding.mockClear();
77
+ const pageData = pageOwnedBy(TENANT_ORG, TENANT_ORG);
78
+
79
+ await applySessionOrgBranding(pageData, undefined, VIEWER_ORG);
80
+
81
+ expect(fetchBranding).not.toHaveBeenCalled();
82
+ expect(pageData.orgId).toBe(TENANT_ORG);
83
+ });
84
+
85
+ test("an anonymous reader is never personalised", async () => {
86
+ fetchBranding.mockClear();
87
+ const pageData = pageOwnedBy(TENANT_ORG, PLATFORM_ORG);
88
+
89
+ await applySessionOrgBranding(pageData, undefined, undefined);
90
+
91
+ expect(fetchBranding).not.toHaveBeenCalled();
92
+ expect(pageData.orgId).toBe(TENANT_ORG);
93
+ });
94
+
95
+ test("a public render mode is never personalised", async () => {
96
+ fetchBranding.mockClear();
97
+ const pageData = pageOwnedBy(TENANT_ORG, PLATFORM_ORG);
98
+
99
+ await applySessionOrgBranding(pageData, "public", VIEWER_ORG);
100
+
101
+ expect(fetchBranding).not.toHaveBeenCalled();
102
+ });
103
+
104
+ test("a genuine PLATFORM page still personalises to the viewer's org", async () => {
105
+ // The behaviour the override exists for: a shared platform portal page,
106
+ // viewed by a member of another org, shows that member their own brand.
107
+ fetchBranding.mockClear();
108
+ const pageData = pageOwnedBy(PLATFORM_ORG, PLATFORM_ORG);
109
+
110
+ await applySessionOrgBranding(pageData, undefined, TENANT_ORG);
111
+
112
+ expect(fetchBranding).toHaveBeenCalledTimes(1);
113
+ expect(pageData.orgId).toBe(TENANT_ORG);
114
+ expect((pageData.brand as { companyName: string }).companyName).toBe(
115
+ "Viewer Org",
116
+ );
117
+ });
118
+ });
@@ -135,6 +135,7 @@ export async function applySessionOrgBranding(
135
135
  renderMode: string | undefined,
136
136
  sessionOrgId: string | undefined,
137
137
  ): Promise<void> {
138
+ const platformOrgId = getPlatformOrgId();
138
139
  if (
139
140
  renderMode !== "public" &&
140
141
  sessionOrgId &&
@@ -144,9 +145,27 @@ export async function applySessionOrgBranding(
144
145
  // (super-admin org). A TENANT-owned site (a customer GPT/quiz/portal)
145
146
  // keeps its page-org brand regardless of the viewer session org. The
146
147
  // old pageData.orgId===website.org mis-fired for a tenant whose page org equals their own website org.
147
- getPlatformOrgId() ===
148
+ platformOrgId ===
148
149
  ((pageData.website as { organisation_id?: string | null } | undefined)
149
- ?.organisation_id ?? undefined)
150
+ ?.organisation_id ?? undefined) &&
151
+ // ...AND the PAGE itself must be platform-owned. A platform-owned
152
+ // WEBSITE also hosts tenant DOCUMENTS — an agreement, a proposal, a
153
+ // signed contract — and those belong to the org that published them.
154
+ // Testing only the website let the viewer's own org repaint a tenant
155
+ // document, which is the same defect this rule already fixed once at
156
+ // the website level (2026-08-17, the Launcha Authority Blueprint).
157
+ //
158
+ // It recurred on sign.scala.business, whose website IS owned by the
159
+ // platform org (c503629a), so the website test passed and every
160
+ // signed-in staff viewer saw a GMG APAC agreement painted in their own
161
+ // org's brand — while an anonymous client saw it correctly. That is
162
+ // the worst possible shape: the only people who cannot see what the
163
+ // client sees are the people reviewing it before it is sent.
164
+ //
165
+ // A public document belongs to the org that PUBLISHED it and must look
166
+ // identical to every reader, so a page whose org is not the platform's
167
+ // is never personalised.
168
+ platformOrgId === (pageData.orgId as string | undefined)
150
169
  ) {
151
170
  try {
152
171
  const branding = await fetchOrganisationPageBranding(