@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
|
@@ -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:
|
|
254
|
+
viewed_at: nowInstant,
|
|
233
255
|
};
|
|
234
256
|
if (executed) {
|
|
235
|
-
data.signed_at =
|
|
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:
|
|
313
|
-
viewed_at:
|
|
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
|
-
|
|
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(
|