@teamscala/service-boot 0.23.520 → 0.23.522

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
@@ -37,9 +37,9 @@
37
37
  "valibot": "*"
38
38
  },
39
39
  "devDependencies": {
40
- "@happy-dom/global-registrator": "20.14.5",
41
- "@testing-library/react": "16.3.3",
42
- "bun-types": "1.4.2"
40
+ "@happy-dom/global-registrator": "*",
41
+ "@testing-library/react": "*",
42
+ "bun-types": "*"
43
43
  },
44
44
  "exports": {
45
45
  "./agreement-gate": "./src/agreement-gate.ts",
@@ -124,6 +124,8 @@
124
124
  "./content-page-floor.ts": "./src/content-page-floor.ts",
125
125
  "./csrf-protection": "./src/csrf-protection.ts",
126
126
  "./csrf-protection.ts": "./src/csrf-protection.ts",
127
+ "./error-page-logo": "./src/error-page-logo.ts",
128
+ "./error-page-logo.ts": "./src/error-page-logo.ts",
127
129
  "./expand-roster": "./src/expand-roster.ts",
128
130
  "./expand-roster.ts": "./src/expand-roster.ts",
129
131
  "./gate-ttls": "./src/gate-ttls.ts",
@@ -300,5 +302,5 @@
300
302
  "repository": "https://github.com/teamgbg/service-boot",
301
303
  "sideEffects": false,
302
304
  "type": "module",
303
- "version": "0.23.520"
305
+ "version": "0.23.522"
304
306
  }
@@ -11,7 +11,7 @@
11
11
  import { getAppLogger } from "@teamscala/logger/app-loggers";
12
12
  import { queryRows } from "@teamscala/db/query-rows";
13
13
  import { createCache } from "@teamscala/cache/create-cache";
14
- import { registerCacheInvalidation } from "@teamscala/db/notify-listener/index";
14
+ import { registerCacheInvalidation } from "@teamscala/db/notify-listener/cache-invalidation";
15
15
 
16
16
  /**
17
17
  * Cache for the page→agreement association (new-flow pages that have no
@@ -4,7 +4,7 @@
4
4
  * ./apply-logger-config declares 1 runtime export(s) and 3 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -25,11 +25,8 @@ import { getAppLogger } from "@teamscala/logger/app-loggers";
25
25
  import { withGateEntryClears } from "./content-gate/gate-entry-clears.ts";
26
26
  import type { Context } from "hono";
27
27
  import { readHtmlTemplate } from "./read-html-template.ts";
28
- import {
29
- PrimaryResourceMissingError,
30
- buildResourceNotFoundResponse,
31
- resolveErrorPageLogo,
32
- } from "./resource-not-found.ts";
28
+ import { resolveErrorPageLogo } from "./error-page-logo.ts";
29
+ import { PrimaryResourceMissingError, buildResourceNotFoundResponse } from "./resource-not-found.ts";
33
30
  import { extractOrgIdFromCallbackPath } from "./resolvers/portal-route-paths.ts";
34
31
 
35
32
  type PageHandler = (
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @status generated — ts-api-smoke template (scala-test-suites
3
+ * templates/ts_api_smoke.pkl @1.3.0) over the exports map of @teamscala/service-boot.
4
+ * ./error-page-logo declares 2 runtime export(s) and 6 declared behaviour vector(s);
5
+ * this suite asserts the import resolves, the runtime surface matches
6
+ * exactly, and every export carries its declared kind. Regenerate from
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
+ * overwritten.
9
+ */
10
+
11
+ import { describe, expect, it } from "bun:test";
12
+
13
+ const mod = await import("./error-page-logo");
14
+
15
+ const { resolveErrorPageLogo, inlineLogoAsDataUri } = mod;
16
+ const __requested: string[] = [];
17
+ let __storageReached = false;
18
+
19
+ describe("error-page-logo exports-map smoke", () => {
20
+ it("the runtime export set is exactly the declared surface", () => {
21
+ expect(Object.keys(mod).sort()).toEqual([
22
+ "inlineLogoAsDataUri",
23
+ "resolveErrorPageLogo",
24
+ ]);
25
+ });
26
+ it("resolveErrorPageLogo is a function", () => {
27
+ expect(typeof mod["resolveErrorPageLogo"]).toBe("function");
28
+ });
29
+ it("inlineLogoAsDataUri is a function", () => {
30
+ expect(typeof mod["inlineLogoAsDataUri"]).toBe("function");
31
+ });
32
+ it("inlines a stored logo as a data URI, fetching the bucket key after the /api/assets/ prefix", async () => {
33
+ __requested.length = 0;
34
+ __storageReached = false;
35
+ const got = await inlineLogoAsDataUri("/api/assets/c503629a/logos/cropped.png", async () => ({ file(key) { __storageReached = true; __requested.push(key); return { stat: async () => ({}), arrayBuffer: async () => new Uint8Array([137, 80, 78, 71]).buffer }; } }));
36
+ expect(__storageReached).toBeTrue();
37
+ expect(__requested).toEqual(["c503629a/logos/cropped.png"]);
38
+ expect(got).toBe("data:image/png;base64,iVBORw==");
39
+ });
40
+ it("a stored value without the /api/assets/ prefix is refused before storage is touched", async () => {
41
+ __requested.length = 0;
42
+ __storageReached = false;
43
+ const got = await inlineLogoAsDataUri("https://cdn.example.com/logo.png", async () => { __storageReached = true; return null; });
44
+ expect(__storageReached).toBeFalse();
45
+ expect(__requested).toEqual([]);
46
+ expect(got).toBeNull();
47
+ });
48
+ it("an unavailable storage client returns null", async () => {
49
+ const got = await inlineLogoAsDataUri("/api/assets/org/logos/cropped.png", async () => null);
50
+ expect(got).toBeNull();
51
+ });
52
+ it("an oversized logo falls back to null instead of bloating the HTML", async () => {
53
+ const got = await inlineLogoAsDataUri("/api/assets/org/logos/huge.png", async () => ({ file() { return { stat: async () => ({}), arrayBuffer: async () => new ArrayBuffer(100 * 1024 + 1) }; } }));
54
+ expect(got).toBeNull();
55
+ });
56
+ it("an empty logo file falls back to null", async () => {
57
+ const got = await inlineLogoAsDataUri("/api/assets/org/logos/empty.png", async () => ({ file() { return { stat: async () => ({}), arrayBuffer: async () => new ArrayBuffer(0) }; } }));
58
+ expect(got).toBeNull();
59
+ });
60
+ it("the stored mime survives inlining — a .jpg key inlines as image/jpeg", async () => {
61
+ const got = await inlineLogoAsDataUri("/api/assets/org/logos/wordmark.jpg", async () => ({ file() { return { stat: async () => ({}), arrayBuffer: async () => new Uint8Array([255, 216, 255]).buffer }; } }));
62
+ expect(got).toBe("data:image/jpeg;base64,/9j/");
63
+ });
64
+ });
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The error/holding page's brand logo: resolve the host org's dark-theme
7
+ * wordmark from its install row and inline it as a data URI. Split from
8
+ * resource-not-found.ts under single-purpose-file (the builder consumes a
9
+ * logoUrl argument; only these resolvers touch storage and the registry).
10
+ *
11
+ * The host org's LOGO (wordmark) for the dark error page — the favicon is a
12
+ * tab glyph, the wrong asset at page scale (operator-observed: a lone "h"
13
+ * where the launcha wordmark belonged). Resolution: the service's own
14
+ * install row names its owner (brand_org); the org's dark-theme logo (a
15
+ * light-coloured mark, correct on the dark page) is preferred, light-theme
16
+ * second. Every step skew-tolerant: any miss → null → the builder renders a
17
+ * logo-less dark page.
18
+ *
19
+ * Returns the logo as an INLINE data URI (not a `/api/assets/` proxy URL) so
20
+ * the branded holding/error page ships the logo IN the initial HTML bytes —
21
+ * no second fetch, no post-paint pop-in, no dependency on the asset proxy the
22
+ * degraded service is failing to serve (incident 2026-07-27: the logo on a
23
+ * DEGRADED render-layer holding page appeared after the page painted). The
24
+ * logo sibling of icons-render-ssr-inline. Size-capped: a pathological
25
+ * (>MAX_INLINE_LOGO_BYTES) logo falls back to null rather than bloating the
26
+ * HTML; the page renders logo-less, never broken.
27
+ */
28
+
29
+ import { getClient } from "@teamscala/object-storage/create/client/get-client";
30
+ import { createCache } from "@teamscala/cache/create-cache";
31
+ import { registerCacheInvalidation } from "@teamscala/db/notify-listener/cache-invalidation";
32
+ import { getServerClient } from "@teamscala/orpc/server-client-registry";
33
+ import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
34
+ import { getAppLogger } from "@teamscala/logger/app-loggers";
35
+ import { ServiceInstallConfigSchema } from "./load-service-install-row.ts";
36
+
37
+ const logoCache = createCache<string | null>("service-boot:error-page-logo", {
38
+ ttlMs: 5 * 60_000,
39
+ maxSize: 8,
40
+ });
41
+
42
+ export async function resolveErrorPageLogo(
43
+ serviceSlug: string,
44
+ ): Promise<string | null> {
45
+ return logoCache.fetchOrCompute(serviceSlug, async () => {
46
+ const log = getAppLogger();
47
+ try {
48
+ const cfg = await loadRegistryConfig(
49
+ "service_install",
50
+ serviceSlug,
51
+ ServiceInstallConfigSchema,
52
+ );
53
+ const org = (cfg as { brand_org?: string }).brand_org;
54
+ if (!org) {
55
+ log.warn(`[brand-logo:${serviceSlug}] no brand_org configured`);
56
+ return null;
57
+ }
58
+ const prisma = (await getServerClient()) as {
59
+ organisation_profile?: {
60
+ findFirst?: (a: unknown) => Promise<unknown>;
61
+ };
62
+ };
63
+ const row = (await prisma.organisation_profile?.findFirst?.({
64
+ where: { id: org },
65
+ select: {
66
+ company_logo_for_dark_theme: true,
67
+ company_logo_for_light_theme: true,
68
+ },
69
+ })) as {
70
+ company_logo_for_dark_theme?: string | null;
71
+ company_logo_for_light_theme?: string | null;
72
+ } | null;
73
+ const stored =
74
+ row?.company_logo_for_dark_theme ||
75
+ row?.company_logo_for_light_theme ||
76
+ null;
77
+ if (!stored) {
78
+ log.error(`[brand-logo:${serviceSlug}] organization ${org} has no logo configured`);
79
+ return null;
80
+ }
81
+ const logo = await inlineLogoAsDataUri(stored);
82
+ if (!logo) {
83
+ log.error(`[brand-logo:${serviceSlug}] failed to load logo`, {
84
+ organizationId: org,
85
+ storedPath: stored,
86
+ });
87
+ }
88
+ return logo;
89
+ } catch (error) {
90
+ log.error(`[brand-logo:${serviceSlug}] failed to resolve organization logo`, {
91
+ error: error instanceof Error ? error.message : String(error),
92
+ });
93
+ return null;
94
+ }
95
+ });
96
+ }
97
+
98
+ /** Above this byte size, inlining bloats the HTML more than it helps — fall
99
+ * back to a logo-less page. Typical org logos are 5–30 KB, so this only
100
+ * catches a genuinely oversized upload. */
101
+ const MAX_INLINE_LOGO_BYTES = 100 * 1024;
102
+
103
+ const ASSETS_PREFIX = "/api/assets/";
104
+
105
+ /**
106
+ * Inline a stored logo value as a `data:<mime>;base64,…` URI by fetching its
107
+ * bytes from S3 (the same store `/api/assets/` proxies to). The stored value
108
+ * IS the servable form — `/api/assets/<key>` — so the bucket key is the path
109
+ * segment after that prefix (single-logo-pathway). Skew-tolerant: any miss →
110
+ * null.
111
+ */
112
+ type LogoStorage = {
113
+ file(key: string): {
114
+ stat(): Promise<unknown>;
115
+ arrayBuffer(): Promise<ArrayBuffer>;
116
+ };
117
+ };
118
+
119
+ export async function inlineLogoAsDataUri(
120
+ stored: string,
121
+ getStorage: () => Promise<LogoStorage | null> = getClient as unknown as () => Promise<LogoStorage | null>,
122
+ ): Promise<string | null> {
123
+ try {
124
+ if (!stored.startsWith(ASSETS_PREFIX)) return null;
125
+ const key = decodeURIComponent(stored.slice(ASSETS_PREFIX.length));
126
+ const s3 = await getStorage();
127
+ if (!s3) return null;
128
+ const file = s3.file(key);
129
+ await file.stat();
130
+ const bytes = await file.arrayBuffer();
131
+ if (bytes.byteLength === 0 || bytes.byteLength > MAX_INLINE_LOGO_BYTES) {
132
+ return null;
133
+ }
134
+ const mime = mimeFromKey(key) ?? "image/png";
135
+ const base64 = Buffer.from(bytes).toString("base64");
136
+ return `data:${mime};base64,${base64}`;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ function mimeFromKey(key: string): string | null {
143
+ const ext = key.split(".").pop()?.toLowerCase();
144
+ switch (ext) {
145
+ case "png":
146
+ return "image/png";
147
+ case "jpg":
148
+ case "jpeg":
149
+ return "image/jpeg";
150
+ case "webp":
151
+ return "image/webp";
152
+ case "gif":
153
+ return "image/gif";
154
+ case "svg":
155
+ return "image/svg+xml";
156
+ case "avif":
157
+ return "image/avif";
158
+ default:
159
+ return null;
160
+ }
161
+ }
162
+
163
+ // Shares the one process-wide LISTEN connection rather than opening its own:
164
+ // `createNotifyListener` is a factory, so a module-scope call holds a
165
+ // connection for the process lifetime (see resolution-caches.ts).
166
+ registerCacheInvalidation(() => {
167
+ logoCache.clear();
168
+ });
@@ -4,7 +4,7 @@
4
4
  * ./page-server-loader-graph declares 1 runtime export(s) and 1 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./meta-title declares 3 runtime export(s) and 4 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./resolve-brand-favicon declares 2 runtime export(s) and 5 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./resolve-theme-mode declares 1 runtime export(s) + 1 type(s) and 5 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./fetch-org-brand declares 1 runtime export(s) and 4 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./website-by-host-map declares 1 runtime export(s) and 7 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -8,11 +8,8 @@
8
8
  // with it into the band.
9
9
 
10
10
  import { describe, expect, it } from "bun:test";
11
- import {
12
- PrimaryResourceMissingError,
13
- buildResourceNotFoundResponse,
14
- inlineLogoAsDataUri,
15
- } from "./resource-not-found.ts";
11
+ import { inlineLogoAsDataUri } from "./error-page-logo.ts";
12
+ import { PrimaryResourceMissingError, buildResourceNotFoundResponse } from "./resource-not-found.ts";
16
13
  import { buildWakingPageHtml } from "@teamscala/os/brand/waking-page-html";
17
14
 
18
15
  describe("PrimaryResourceMissingError", () => {
@@ -54,24 +51,6 @@ describe("buildResourceNotFoundResponse", () => {
54
51
  expect(logo).toBe("data:image/png;base64,iVBORw==");
55
52
  });
56
53
 
57
- it("passes the resolved logo to the waking page instead of its placeholder", async () => {
58
- const logo = await inlineLogoAsDataUri(
59
- "/api/assets/c503629a/logos/cropped.png",
60
- async () => ({
61
- file() {
62
- return {
63
- stat: async () => ({}),
64
- arrayBuffer: async () => new Uint8Array([137, 80, 78, 71]).buffer,
65
- };
66
- },
67
- }),
68
- );
69
- const html = buildWakingPageHtml({ logoUrl: logo });
70
- expect(logo).toStartWith("data:image/png;base64,");
71
- expect(html).toContain('<img src="data:image/png;base64,iVBORw=="');
72
- expect(html).not.toContain('class="uif-logo-loading-pulse" aria-hidden="true"');
73
- });
74
-
75
54
  it("returns a 404 Response for a dead link", () => {
76
55
  const res = buildResourceNotFoundResponse("project");
77
56
  expect(res).toBeInstanceOf(Response);
@@ -3,10 +3,10 @@
3
3
  * @status handwritten
4
4
  * @edit edit directly
5
5
  *
6
- * One cohesive unit — the branded resource-not-found / holding page: the
7
- * PrimaryResourceMissingError trigger, the org-logo resolver the page
8
- * renders, and the HTML builder are tightly coupled (the builder consumes
9
- * the logo; the error drives the page) and cannot be split without churn.
6
+ * One cohesive unit — the branded resource-not-found / holding page FLOOR:
7
+ * the PrimaryResourceMissingError trigger and the HTML builder (the builder
8
+ * consumes a logoUrl argument; the org-logo resolvers live in
9
+ * error-page-logo.ts).
10
10
  *
11
11
  * Branded 404 for registry page routes whose PRIMARY resource is missing.
12
12
  *
@@ -27,33 +27,7 @@
27
27
  * floor that must always exist.
28
28
  */
29
29
 
30
- /**
31
- * The host org's LOGO (wordmark) for the dark error page — the favicon is a
32
- * tab glyph, the wrong asset at page scale (operator-observed: a lone "h"
33
- * where the launcha wordmark belonged). Resolution: the service's own
34
- * install row names its owner (brand_org); the org's dark-theme logo (a
35
- * light-coloured mark, correct on the dark page) is preferred, light-theme
36
- * second. Every step skew-tolerant: any miss → null → the builder renders a
37
- * logo-less dark page.
38
- *
39
- * Returns the logo as an INLINE data URI (not a `/api/assets/` proxy URL) so
40
- * the branded holding/error page ships the logo IN the initial HTML bytes —
41
- * no second fetch, no post-paint pop-in, no dependency on the asset proxy the
42
- * degraded service is failing to serve (incident 2026-07-27: the logo on a
43
- * DEGRADED render-layer holding page appeared after the page painted). The
44
- * logo sibling of icons-render-ssr-inline. Size-capped: a pathological
45
- * (>MAX_INLINE_LOGO_BYTES) logo falls back to null rather than bloating the
46
- * HTML; the page renders logo-less, never broken.
47
- */
48
-
49
- import { getClient } from "@teamscala/object-storage/create/client/get-client";
50
- import { createCache } from "@teamscala/cache/create-cache";
51
- import { registerCacheInvalidation } from "@teamscala/db/notify-listener/index";
52
- import { getServerClient } from "@teamscala/orpc/server-client-registry";
53
- import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
54
- import { getAppLogger } from "@teamscala/logger/app-loggers";
55
30
  import { brandLogoImgTag } from "@teamscala/os/brand/brand-logo-img-tag";
56
- import { ServiceInstallConfigSchema } from "./load-service-install-row.ts";
57
31
 
58
32
  const RESOURCE_LABELS: Record<string, string> = {
59
33
  proposal: "proposal",
@@ -63,132 +37,6 @@ const RESOURCE_LABELS: Record<string, string> = {
63
37
  agent: "assistant",
64
38
  };
65
39
 
66
- const logoCache = createCache<string | null>("service-boot:error-page-logo", {
67
- ttlMs: 5 * 60_000,
68
- maxSize: 8,
69
- });
70
-
71
- export async function resolveErrorPageLogo(
72
- serviceSlug: string,
73
- ): Promise<string | null> {
74
- return logoCache.fetchOrCompute(serviceSlug, async () => {
75
- const log = getAppLogger();
76
- try {
77
- const cfg = await loadRegistryConfig(
78
- "service_install",
79
- serviceSlug,
80
- ServiceInstallConfigSchema,
81
- );
82
- const org = (cfg as { brand_org?: string }).brand_org;
83
- if (!org) {
84
- log.warn(`[brand-logo:${serviceSlug}] no brand_org configured`);
85
- return null;
86
- }
87
- const prisma = (await getServerClient()) as {
88
- organisation_profile?: {
89
- findFirst?: (a: unknown) => Promise<unknown>;
90
- };
91
- };
92
- const row = (await prisma.organisation_profile?.findFirst?.({
93
- where: { id: org },
94
- select: {
95
- company_logo_for_dark_theme: true,
96
- company_logo_for_light_theme: true,
97
- },
98
- })) as {
99
- company_logo_for_dark_theme?: string | null;
100
- company_logo_for_light_theme?: string | null;
101
- } | null;
102
- const stored =
103
- row?.company_logo_for_dark_theme ||
104
- row?.company_logo_for_light_theme ||
105
- null;
106
- if (!stored) {
107
- log.error(`[brand-logo:${serviceSlug}] organization ${org} has no logo configured`);
108
- return null;
109
- }
110
- const logo = await inlineLogoAsDataUri(stored);
111
- if (!logo) {
112
- log.error(`[brand-logo:${serviceSlug}] failed to load logo`, {
113
- organizationId: org,
114
- storedPath: stored,
115
- });
116
- }
117
- return logo;
118
- } catch (error) {
119
- log.error(`[brand-logo:${serviceSlug}] failed to resolve organization logo`, {
120
- error: error instanceof Error ? error.message : String(error),
121
- });
122
- return null;
123
- }
124
- });
125
- }
126
-
127
- /** Above this byte size, inlining bloats the HTML more than it helps — fall
128
- * back to a logo-less page. Typical org logos are 5–30 KB, so this only
129
- * catches a genuinely oversized upload. */
130
- const MAX_INLINE_LOGO_BYTES = 100 * 1024;
131
-
132
- const ASSETS_PREFIX = "/api/assets/";
133
-
134
- /**
135
- * Inline a stored logo value as a `data:<mime>;base64,…` URI by fetching its
136
- * bytes from S3 (the same store `/api/assets/` proxies to). The stored value
137
- * IS the servable form — `/api/assets/<key>` — so the bucket key is the path
138
- * segment after that prefix (single-logo-pathway). Skew-tolerant: any miss →
139
- * null.
140
- */
141
- type LogoStorage = {
142
- file(key: string): {
143
- stat(): Promise<unknown>;
144
- arrayBuffer(): Promise<ArrayBuffer>;
145
- };
146
- };
147
-
148
- export async function inlineLogoAsDataUri(
149
- stored: string,
150
- getStorage: () => Promise<LogoStorage | null> = getClient as unknown as () => Promise<LogoStorage | null>,
151
- ): Promise<string | null> {
152
- try {
153
- if (!stored.startsWith(ASSETS_PREFIX)) return null;
154
- const key = decodeURIComponent(stored.slice(ASSETS_PREFIX.length));
155
- const s3 = await getStorage();
156
- if (!s3) return null;
157
- const file = s3.file(key);
158
- await file.stat();
159
- const bytes = await file.arrayBuffer();
160
- if (bytes.byteLength === 0 || bytes.byteLength > MAX_INLINE_LOGO_BYTES) {
161
- return null;
162
- }
163
- const mime = mimeFromKey(key) ?? "image/png";
164
- const base64 = Buffer.from(bytes).toString("base64");
165
- return `data:${mime};base64,${base64}`;
166
- } catch {
167
- return null;
168
- }
169
- }
170
-
171
- function mimeFromKey(key: string): string | null {
172
- const ext = key.split(".").pop()?.toLowerCase();
173
- switch (ext) {
174
- case "png":
175
- return "image/png";
176
- case "jpg":
177
- case "jpeg":
178
- return "image/jpeg";
179
- case "webp":
180
- return "image/webp";
181
- case "gif":
182
- return "image/gif";
183
- case "svg":
184
- return "image/svg+xml";
185
- case "avif":
186
- return "image/avif";
187
- default:
188
- return null;
189
- }
190
- }
191
-
192
40
  export class PrimaryResourceMissingError extends Error {
193
41
  readonly resourceType: string;
194
42
  // HTTP status for the holding page. 404 for a genuinely missing/invalid
@@ -289,10 +137,3 @@ export function buildResourceNotFoundResponse(
289
137
  },
290
138
  });
291
139
  }
292
-
293
- // Shares the one process-wide LISTEN connection rather than opening its own:
294
- // `createNotifyListener` is a factory, so a module-scope call holds a
295
- // connection for the process lifetime (see resolution-caches.ts).
296
- registerCacheInvalidation(() => {
297
- logoCache.clear();
298
- });
@@ -4,7 +4,7 @@
4
4
  * ./run-boot-hook-list declares 1 runtime export(s) and 4 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./run-boot-hooks declares 1 runtime export(s) + 2 type(s) and 5 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./scala-tools-currency-watchdog declares 1 runtime export(s) and 2 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./two-phase-minimal-app declares 1 runtime export(s) + 1 type(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -70,10 +70,8 @@ import { readinessGate } from "./readiness-gate.ts";
70
70
  import { registerStaticAssets } from "./register-static-assets";
71
71
  import { registerWsRoutes } from "./register-ws-routes.ts";
72
72
  import { createRequestErrorHandler } from "./hono-error-handler.ts";
73
- import {
74
- buildResourceNotFoundResponse,
75
- resolveErrorPageLogo,
76
- } from "./resource-not-found.ts";
73
+ import { resolveErrorPageLogo } from "./error-page-logo.ts";
74
+ import { buildResourceNotFoundResponse } from "./resource-not-found.ts";
77
75
  import { injectSecurityHeaders } from "./security-headers.ts";
78
76
  import { registerPageRoutes } from "./register-page-routes.ts";
79
77
  import { runBootHookList } from "./run-boot-hook-list.ts";
@@ -4,7 +4,7 @@
4
4
  * ./two-phase-readiness declares 6 runtime export(s) and 8 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -13,8 +13,8 @@ import { mock } from "bun:test";
13
13
  const __realServerClient = await import("./page-server-orpc/server-client.ts");
14
14
  const __capabilityFault: { value: string | null } = { value: null };
15
15
  mock.module("./page-server-orpc/server-client.ts", () => ({ ...__realServerClient, getServerClientCapabilityFault: () => __capabilityFault.value }));
16
- const __realNotFound = await import("./resource-not-found.ts");
17
- mock.module("./resource-not-found.ts", () => ({ ...__realNotFound, resolveErrorPageLogo: async () => "https://logo.local/light.png" }));
16
+ const __realLogo = await import("./error-page-logo.ts");
17
+ mock.module("./error-page-logo.ts", () => ({ ...__realLogo, resolveErrorPageLogo: async () => "https://logo.local/light.png" }));
18
18
  const __ctx = { json: (body: unknown, status?: number, headers?: Record<string, string>) => new Response(JSON.stringify(body), { status: status ?? 200, headers }) };
19
19
 
20
20
  const mod = await import("./two-phase-readiness");
@@ -92,7 +92,7 @@ import { buildWakingPageHtml } from "@teamscala/os/brand/waking-page-html";
92
92
  import { WAKING_RETRY_SECONDS } from "@teamscala/os/brand/waking-page-budget.generated";
93
93
  import type { Context } from "hono";
94
94
  import { getServerClientCapabilityFault } from "./page-server-orpc/server-client.ts";
95
- import { resolveErrorPageLogo } from "./resource-not-found.ts";
95
+ import { resolveErrorPageLogo } from "./error-page-logo.ts";
96
96
  import type { CreatePageHandler } from "./types.ts";
97
97
 
98
98
  // Module-level storage for the real page handler, updated when Phase 2 completes.
package/src/types.test.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * ./types declares 0 runtime export(s) + 5 type(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./ui-service declares 1 runtime export(s) + 3 type(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10
 
@@ -4,7 +4,7 @@
4
4
  * ./ws-boot-self-test declares 1 runtime export(s) and 3 declared behaviour vector(s);
5
5
  * this suite asserts the import resolves, the runtime surface matches
6
6
  * exactly, and every export carries its declared kind. Regenerate from
7
- * inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
7
+ * inputs/server-service-boot-ts/api_smoke.json (scala-test-suites) — hand edits are
8
8
  * overwritten.
9
9
  */
10
10