@teamscala/service-boot 0.23.391 → 0.23.394

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.391",
9
+ "version": "0.23.394",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -34,9 +34,11 @@
34
34
  "@teamscala/generic-registry": "*",
35
35
  "@teamscala/http": "*",
36
36
  "@teamscala/logger": "*",
37
+ "@teamscala/notify-cache": "*",
37
38
  "@teamscala/object-storage": "*",
38
39
  "@teamscala/orpc": "*",
39
40
  "@teamscala/os": "*",
41
+ "@teamscala/publish-registry": "*",
40
42
  "@teamscala/realtime-utils": "*",
41
43
  "@teamscala/resolver-engine": "*",
42
44
  "@teamscala/timing": "*",
@@ -562,6 +564,14 @@
562
564
  "types": "./src/load-service-prisma-client.d.ts",
563
565
  "default": "./src/load-service-prisma-client.ts"
564
566
  },
567
+ "./module-not-found": {
568
+ "types": "./src/module-not-found.d.ts",
569
+ "default": "./src/module-not-found.ts"
570
+ },
571
+ "./module-not-found.ts": {
572
+ "types": "./src/module-not-found.d.ts",
573
+ "default": "./src/module-not-found.ts"
574
+ },
565
575
  "./mount": {
566
576
  "types": "./src/mount.d.ts",
567
577
  "default": "./src/mount.ts"
@@ -119,7 +119,6 @@ async function sendAuthorityReportEmail(opts: {
119
119
  categoryScores: Record<string, number>;
120
120
  ctaUrl?: string;
121
121
  organisationId?: string | null;
122
- brand?: import("@teamscala/email/identity/resolve-email-sender").OrgBrandTokens;
123
122
  prisma: any;
124
123
  }): Promise<void> {
125
124
  const log = getAppLogger();
@@ -137,7 +136,23 @@ async function sendAuthorityReportEmail(opts: {
137
136
  }
138
137
  initResendClient(decrypted.token);
139
138
 
140
- const sender = await resolveEmailSender(opts.organisationId ?? null);
139
+ const brandRow =
140
+ opts.organisationId && opts.prisma
141
+ ? await fetchOrgBrand(
142
+ opts.prisma as Parameters<typeof fetchOrgBrand>[0],
143
+ opts.organisationId,
144
+ ).catch(() => null)
145
+ : null;
146
+ const sender = await resolveEmailSender(opts.organisationId ?? null, {
147
+ brand: brandRow
148
+ ? {
149
+ orgName: brandRow.companyName ?? "",
150
+ primaryColor: brandRow.primaryColor,
151
+ accentColor: brandRow.accentColor ?? null,
152
+ logoUrl: brandRow.logo.light ?? null,
153
+ }
154
+ : undefined,
155
+ });
141
156
  const primary = sender.brand.primaryColor;
142
157
  const muted = "#6b7280"; // structural neutral for body labels
143
158
  const logoUrl = sender.brand.logoUrl;
@@ -441,21 +456,6 @@ export const publicAssessmentRouter = {
441
456
  (typeof assessment.cta_url === "string" && assessment.cta_url) ||
442
457
  undefined;
443
458
  if (band) {
444
- const prisma = (context as { prisma: unknown }).prisma;
445
- const brandRow = opts.organisationId
446
- ? await fetchOrgBrand(
447
- prisma as Parameters<typeof fetchOrgBrand>[0],
448
- opts.organisationId,
449
- ).catch(() => null)
450
- : null;
451
- const brand = brandRow
452
- ? {
453
- orgName: brandRow.companyName ?? "",
454
- primaryColor: brandRow.primaryColor,
455
- accentColor: brandRow.accentColor ?? null,
456
- logoUrl: brandRow.logo.light ?? null,
457
- }
458
- : undefined;
459
459
  void sendAuthorityReportEmail({
460
460
  to: email,
461
461
  firstName,
@@ -463,8 +463,7 @@ export const publicAssessmentRouter = {
463
463
  bandLabel: band.label,
464
464
  categoryScores,
465
465
  ctaUrl,
466
- organisationId: opts.organisationId,
467
- brand,
466
+ organisationId: orgId,
468
467
  prisma,
469
468
  }).catch((e) =>
470
469
  log.error(
@@ -24,9 +24,10 @@
24
24
  import type { ScheduledJobConfig } from "@teamscala/db-validation/registry-schemas/scheduled-job";
25
25
  import { recordEvent } from "@teamscala/event-log/record-event";
26
26
  import { getAppLogger } from "@teamscala/logger/app-loggers";
27
+ import { getDistTagVersion } from "@teamscala/publish-registry";
27
28
  import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
28
29
 
29
- const BUN_NPM_LATEST = "https://registry.npmjs.org/bun/latest";
30
+ const NPM_REGISTRY = "https://registry.npmjs.org";
30
31
  const TARGET_STABLE = "1.4.0"; // flip back when a stable at or above this ships
31
32
 
32
33
  interface BunLatestManifest {
@@ -54,23 +55,19 @@ function isStableAtOrAbove(version: string, target: string): boolean {
54
55
 
55
56
  async function bunStableWatch(_ctx: { config: ScheduledJobConfig }): Promise<void> {
56
57
  const log = getAppLogger();
57
- let manifest: BunLatestManifest;
58
- try {
59
- const res = await fetch(BUN_NPM_LATEST, { headers: { accept: "application/json" } });
60
- if (!res.ok) throw new Error(`npm bun/latest HTTP ${res.status}`);
61
- manifest = (await res.json()) as BunLatestManifest;
62
- } catch (err) {
63
- log.warn(`[bun-stable-watch] failed to fetch npm bun/latest: ${err}`);
58
+ const version = await getDistTagVersion("bun", "latest", NPM_REGISTRY);
59
+ if (!version) {
60
+ log.warn("[bun-stable-watch] failed to fetch npm bun/latest");
64
61
  return;
65
62
  }
66
- if (isStableAtOrAbove(manifest.version, TARGET_STABLE)) {
63
+ if (isStableAtOrAbove(version, TARGET_STABLE)) {
67
64
  log.warn(
68
- `[bun-stable-watch] Bun stable ${manifest.version} (>=${TARGET_STABLE}) shipped — escape hatch 'bun-14-lock-resolution-pin' is ready to retire. Run the atomic flip-back: config/scala-tools-deps.lockResolutionBun -> "bun" + railpack_config rows -> 1.4.0 stable + delete /usr/local/bin/bun-1.3.14 + a green deploy.`,
65
+ `[bun-stable-watch] Bun stable ${version} (>=${TARGET_STABLE}) shipped — escape hatch 'bun-14-lock-resolution-pin' is ready to retire. Run the atomic flip-back: config/scala-tools-deps.lockResolutionBun -> "bun" + railpack_config rows -> 1.4.0 stable + delete /usr/local/bin/bun-1.3.14 + a green deploy.`,
69
66
  );
70
67
  recordEvent({
71
68
  kind: "bun.stable.shipped",
72
69
  payload: {
73
- latest: manifest.version,
70
+ latest: version,
74
71
  targetStable: TARGET_STABLE,
75
72
  note: "escape hatch bun-14-lock-resolution-pin ready to retire; verify railpack 1.4.0 support then run the atomic flip-back",
76
73
  },
@@ -78,11 +75,11 @@ async function bunStableWatch(_ctx: { config: ScheduledJobConfig }): Promise<voi
78
75
  return;
79
76
  }
80
77
  log.info(
81
- `[bun-stable-watch] npm bun/latest = ${manifest.version} — not yet stable ${TARGET_STABLE}; escape hatch remains`,
78
+ `[bun-stable-watch] npm bun/latest = ${version} — not yet stable ${TARGET_STABLE}; escape hatch remains`,
82
79
  );
83
80
  recordEvent({
84
81
  kind: "bun.watch.tick",
85
- payload: { latest: manifest.version, targetStable: TARGET_STABLE },
82
+ payload: { latest: version, targetStable: TARGET_STABLE },
86
83
  });
87
84
  }
88
85
 
@@ -28,6 +28,7 @@ import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
28
28
  import { recordEvent } from "@teamscala/event-log/record-event";
29
29
  import { getAppLogger } from "@teamscala/logger/app-loggers";
30
30
  import { spawn } from "@teamscala/os/spawn";
31
+ import { getDistTagVersion } from "@teamscala/publish-registry";
31
32
  import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
32
33
 
33
34
  /**
@@ -180,7 +181,7 @@ async function fetchLatestGithubRelease(repo: string): Promise<string | null> {
180
181
  async function fetchLatestVersion(pkg: string, distTag: string): Promise<string | null> {
181
182
  try {
182
183
  // @teamscala/* packages use the internal dist-tag tool; public npm packages
183
- // fetch directly from the npm registry (same pattern as bun-stable-watch).
184
+ // fetch directly from the npm registry via the typed primitive.
184
185
  if (pkg.startsWith("@teamscala/")) {
185
186
  const result = await spawn({
186
187
  name: "singleton-tools-currency:dist-tag",
@@ -196,12 +197,7 @@ async function fetchLatestVersion(pkg: string, distTag: string): Promise<string
196
197
  if (!latest || !parseSemver(latest)) return null;
197
198
  return latest;
198
199
  }
199
- // Public npm package: fetch the dist-tag manifest directly.
200
- const url = `https://registry.npmjs.org/${encodeURIComponent(pkg)}/${encodeURIComponent(distTag)}`;
201
- const res = await fetch(url, { headers: { accept: "application/json" } });
202
- if (!res.ok) return null;
203
- const manifest = (await res.json()) as { version: string };
204
- return manifest.version ?? null;
200
+ return getDistTagVersion(pkg, distTag, "https://registry.npmjs.org");
205
201
  } catch {
206
202
  return null;
207
203
  }
@@ -39,6 +39,7 @@
39
39
  * bytes across restarts).
40
40
  */
41
41
 
42
+ import { createCache } from "@teamscala/cache/create-cache";
42
43
  import { readClientBuildStatus } from "./build-client-assets.ts";
43
44
  import { join } from "node:path";
44
45
 
@@ -70,7 +71,15 @@ const PAGE_DOCUMENT = `<!doctype html>
70
71
  </body>
71
72
  </html>`;
72
73
 
73
- let cached: { html: string; manifestMtime: number } | null = null;
74
+ interface CachedHtml {
75
+ html: string;
76
+ manifestMtime: number;
77
+ }
78
+
79
+ const htmlCache = createCache<CachedHtml>("service-boot:html-template", {
80
+ ttlMs: Number.POSITIVE_INFINITY,
81
+ maxSize: 1,
82
+ });
74
83
 
75
84
  function manifestPath(): string {
76
85
  return join(process.cwd(), "dist", "client", "asset-manifest.json");
@@ -98,8 +107,9 @@ async function loadManifest(): Promise<AssetManifest> {
98
107
  }
99
108
 
100
109
  export async function readHtmlTemplate(): Promise<string> {
101
- const mtime = await manifestMtimeMs();
102
- if (cached && cached.manifestMtime === mtime) return cached.html;
110
+ const manifestMtime = await manifestMtimeMs();
111
+ const cached = htmlCache.get("default");
112
+ if (cached && cached.manifestMtime === manifestMtime) return cached.html;
103
113
  const manifest = await loadManifest();
104
114
  let html = PAGE_DOCUMENT
105
115
  .replace(/\{\{stylesPath\}\}/g, manifest.stylesPath)
@@ -116,6 +126,6 @@ export async function readHtmlTemplate(): Promise<string> {
116
126
  ? html.replace("</head>", `${meta}</head>`)
117
127
  : `${meta}${html}`;
118
128
  }
119
- cached = { html, manifestMtime: mtime };
129
+ htmlCache.set("default", { html, manifestMtime });
120
130
  return html;
121
131
  }
@@ -5,6 +5,8 @@
5
5
  *
6
6
  * Polls the installed scala-tools package against GitHub Packages and repairs
7
7
  * stale daemon tooling by reinstalling and letting PM2 restart the process.
8
+ * The fetch routes through `getGithubPackagesLatestVersion` so the URL
9
+ * encoding and GitHub-Packages auth header live in the typed primitive.
8
10
  */
9
11
 
10
12
  import { recordEvent } from "@teamscala/event-log/record-event";
@@ -14,6 +16,7 @@ import { createWatchdog } from "@teamscala/watchdog/create-watchdog";
14
16
  import type { WatchdogHandle } from "@teamscala/watchdog/types";
15
17
  import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
16
18
  import { decryptSecretConfig } from "@teamscala/encryption/crypto/secret-config";
19
+ import { getGithubPackagesLatestVersion } from "@teamscala/publish-registry";
17
20
  import * as v from "valibot";
18
21
 
19
22
  const PACKAGE_NAME = "@teamscala/scala-tools";
@@ -55,10 +58,6 @@ interface PackageManifest {
55
58
  version?: unknown;
56
59
  }
57
60
 
58
- interface PackagePackument {
59
- "dist-tags"?: { latest?: unknown };
60
- }
61
-
62
61
  let watchdog: WatchdogHandle | null = null;
63
62
 
64
63
  async function readInstalledVersion(cwd: string): Promise<string | null> {
@@ -70,24 +69,25 @@ async function readInstalledVersion(cwd: string): Promise<string | null> {
70
69
  }
71
70
  }
72
71
 
73
- async function readLatestVersion(fetchTimeoutMs: number): Promise<string | null> {
72
+ /**
73
+ * PACKAGE_URL is `<registry>/<encoded-name>`. Split it so the typed primitive
74
+ * owns the URL encoding (encodeURIComponent) and the GitHub-Packages auth
75
+ * header shape — neither is this file's concern.
76
+ */
77
+ function registryFromPackageUrl(pkgUrl: string): { registry: string; name: string } {
78
+ const lastSlash = pkgUrl.lastIndexOf("/");
79
+ if (lastSlash < 0) return { registry: pkgUrl, name: "" };
80
+ return {
81
+ registry: pkgUrl.slice(0, lastSlash),
82
+ name: decodeURIComponent(pkgUrl.slice(lastSlash + 1)),
83
+ };
84
+ }
85
+
86
+ async function readLatestVersion(_fetchTimeoutMs: number): Promise<string | null> {
74
87
  const token = await getGithubPackagesToken();
75
88
  if (!token) return null;
76
- try {
77
- const response = await fetch(PACKAGE_URL, {
78
- headers: {
79
- accept: "application/vnd.github+json",
80
- authorization: `Bearer ${token}`,
81
- },
82
- signal: AbortSignal.timeout(fetchTimeoutMs),
83
- });
84
- if (!response.ok) return null;
85
- const packument = (await response.json()) as PackagePackument;
86
- const latest = packument["dist-tags"]?.latest;
87
- return typeof latest === "string" ? latest : null;
88
- } catch {
89
- return null;
90
- }
89
+ const { registry, name } = registryFromPackageUrl(PACKAGE_URL);
90
+ return getGithubPackagesLatestVersion(name, token, registry);
91
91
  }
92
92
 
93
93
  async function checkCurrency(): Promise<{ healthy: boolean; details?: Record<string, unknown> }> {