@teamscala/service-boot 0.23.348 → 0.23.350
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
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* the response builder are pure (no S3), so the behaviour is exercised directly.
|
|
8
8
|
*/
|
|
9
9
|
import { describe, expect, it } from "bun:test";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
PrimaryResourceMissingError,
|
|
12
|
+
buildResourceNotFoundResponse,
|
|
13
|
+
inlineLogoAsDataUri,
|
|
14
|
+
} from "./resource-not-found.ts";
|
|
15
|
+
import { buildWakingPageHtml } from "@teamscala/os/brand/waking-page-html";
|
|
11
16
|
|
|
12
17
|
describe("PrimaryResourceMissingError", () => {
|
|
13
18
|
it("is an Error carrying resourceType + httpStatus", () => {
|
|
@@ -29,6 +34,43 @@ describe("PrimaryResourceMissingError", () => {
|
|
|
29
34
|
});
|
|
30
35
|
|
|
31
36
|
describe("buildResourceNotFoundResponse", () => {
|
|
37
|
+
it("loads the organization logo from its stored upload path", async () => {
|
|
38
|
+
const bytes = new Uint8Array([137, 80, 78, 71]);
|
|
39
|
+
const requested: string[] = [];
|
|
40
|
+
const logo = await inlineLogoAsDataUri(
|
|
41
|
+
"/api/files/c503629a/logos/cropped.png",
|
|
42
|
+
async () => ({
|
|
43
|
+
file(key) {
|
|
44
|
+
requested.push(key);
|
|
45
|
+
return {
|
|
46
|
+
stat: async () => ({}),
|
|
47
|
+
arrayBuffer: async () => bytes.buffer,
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
expect(requested).toEqual(["c503629a/logos/cropped.png"]);
|
|
53
|
+
expect(logo).toBe("data:image/png;base64,iVBORw==");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("passes the resolved logo to the waking page instead of its placeholder", async () => {
|
|
57
|
+
const logo = await inlineLogoAsDataUri(
|
|
58
|
+
"/api/files/c503629a/logos/cropped.png",
|
|
59
|
+
async () => ({
|
|
60
|
+
file() {
|
|
61
|
+
return {
|
|
62
|
+
stat: async () => ({}),
|
|
63
|
+
arrayBuffer: async () => new Uint8Array([137, 80, 78, 71]).buffer,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
const html = buildWakingPageHtml({ logoUrl: logo });
|
|
69
|
+
expect(logo).toStartWith("data:image/png;base64,");
|
|
70
|
+
expect(html).toContain('<img src="data:image/png;base64,iVBORw=="');
|
|
71
|
+
expect(html).not.toContain('class="uif-logo-loading-pulse" aria-hidden="true"');
|
|
72
|
+
});
|
|
73
|
+
|
|
32
74
|
it("returns a 404 Response for a dead link", () => {
|
|
33
75
|
const res = buildResourceNotFoundResponse("project");
|
|
34
76
|
expect(res).toBeInstanceOf(Response);
|
|
@@ -34,6 +34,7 @@ import { createNotifyListener, getDirectDatabaseUrl } from "@teamscala/db/notify
|
|
|
34
34
|
import { getServerClient } from "@teamscala/orpc/server-client-registry";
|
|
35
35
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
36
36
|
import { toAssetPath } from "@teamscala/formatters/asset-url";
|
|
37
|
+
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
37
38
|
import { brandLogoImgTag } from "@teamscala/os/brand/brand-logo-img-tag";
|
|
38
39
|
import { ServiceInstallConfigSchema } from "./load-service-install-row.ts";
|
|
39
40
|
|
|
@@ -72,6 +73,7 @@ export async function resolveErrorPageLogo(
|
|
|
72
73
|
serviceSlug: string,
|
|
73
74
|
): Promise<string | null> {
|
|
74
75
|
return logoCache.fetchOrCompute(serviceSlug, async () => {
|
|
76
|
+
const log = getAppLogger();
|
|
75
77
|
try {
|
|
76
78
|
const cfg = await loadRegistryConfig(
|
|
77
79
|
"service_install",
|
|
@@ -79,7 +81,10 @@ export async function resolveErrorPageLogo(
|
|
|
79
81
|
ServiceInstallConfigSchema,
|
|
80
82
|
);
|
|
81
83
|
const org = (cfg as { brand_org?: string }).brand_org;
|
|
82
|
-
if (!org)
|
|
84
|
+
if (!org) {
|
|
85
|
+
log.warn(`[brand-logo:${serviceSlug}] no brand_org configured`);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
83
88
|
const prisma = (await getServerClient()) as {
|
|
84
89
|
organisation_profile?: {
|
|
85
90
|
findFirst?: (a: unknown) => Promise<unknown>;
|
|
@@ -99,8 +104,22 @@ export async function resolveErrorPageLogo(
|
|
|
99
104
|
row?.company_logo_for_dark_theme ||
|
|
100
105
|
row?.company_logo_for_light_theme ||
|
|
101
106
|
null;
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
if (!stored) {
|
|
108
|
+
log.error(`[brand-logo:${serviceSlug}] organization ${org} has no logo configured`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const logo = await inlineLogoAsDataUri(stored);
|
|
112
|
+
if (!logo) {
|
|
113
|
+
log.error(`[brand-logo:${serviceSlug}] failed to load logo`, {
|
|
114
|
+
organizationId: org,
|
|
115
|
+
storedPath: stored,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return logo;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
log.error(`[brand-logo:${serviceSlug}] failed to resolve organization logo`, {
|
|
121
|
+
error: error instanceof Error ? error.message : String(error),
|
|
122
|
+
});
|
|
104
123
|
return null;
|
|
105
124
|
}
|
|
106
125
|
});
|
|
@@ -120,16 +139,26 @@ const ASSETS_PREFIX = "/api/assets/";
|
|
|
120
139
|
* key, `/api/files/`, tigris URL) to `/api/assets/<key>`, and the proxy
|
|
121
140
|
* decodes that path segment to the S3 key. Skew-tolerant: any miss → null.
|
|
122
141
|
*/
|
|
123
|
-
|
|
142
|
+
type LogoStorage = {
|
|
143
|
+
file(key: string): {
|
|
144
|
+
stat(): Promise<unknown>;
|
|
145
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export async function inlineLogoAsDataUri(
|
|
124
150
|
stored: string,
|
|
151
|
+
getStorage: () => Promise<LogoStorage | null> = getClient as unknown as () => Promise<LogoStorage | null>,
|
|
125
152
|
): Promise<string | null> {
|
|
126
153
|
try {
|
|
127
154
|
const assetPath = toAssetPath(stored);
|
|
128
155
|
if (!assetPath.startsWith(ASSETS_PREFIX)) return null;
|
|
129
156
|
const key = decodeURIComponent(assetPath.slice(ASSETS_PREFIX.length));
|
|
130
|
-
const s3 = await
|
|
157
|
+
const s3 = await getStorage();
|
|
131
158
|
if (!s3) return null;
|
|
132
|
-
const
|
|
159
|
+
const file = s3.file(key);
|
|
160
|
+
await file.stat();
|
|
161
|
+
const bytes = await file.arrayBuffer();
|
|
133
162
|
if (bytes.byteLength === 0 || bytes.byteLength > MAX_INLINE_LOGO_BYTES) {
|
|
134
163
|
return null;
|
|
135
164
|
}
|