@teamscala/service-boot 0.23.348 → 0.23.349

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
@@ -1062,5 +1062,5 @@
1062
1062
  "repository": "https://github.com/teamgbg/service-boot",
1063
1063
  "sideEffects": false,
1064
1064
  "type": "module",
1065
- "version": "0.23.348"
1065
+ "version": "0.23.349"
1066
1066
  }
@@ -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 { PrimaryResourceMissingError, buildResourceNotFoundResponse } from "./resource-not-found.ts";
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) return null;
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
- return stored ? inlineLogoAsDataUri(stored) : null;
103
- } catch {
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
- async function inlineLogoAsDataUri(
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 getClient();
157
+ const s3 = await getStorage();
131
158
  if (!s3) return null;
132
- const bytes = await s3.file(key).arrayBuffer();
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
  }