@teamscala/service-boot 0.23.434 → 0.23.436
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
|
@@ -21,8 +21,6 @@
|
|
|
21
21
|
* changes (`migrations-copy-not-rewrite`).
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { createHash } from "node:crypto";
|
|
25
|
-
import { existsSync } from "node:fs";
|
|
26
24
|
import { dirname, join, relative, resolve } from "node:path";
|
|
27
25
|
import { firstPartyScopeAlternation } from "@teamscala/os/workspace-topology/first-party-scopes";
|
|
28
26
|
|
|
@@ -32,7 +30,7 @@ import { firstPartyScopeAlternation } from "@teamscala/os/workspace-topology/fir
|
|
|
32
30
|
// blk-utilities changes undetected → stale bundle). Cycle-safe via `seen`.
|
|
33
31
|
export async function hashResolvedPackageStylesheets(
|
|
34
32
|
cwd: string,
|
|
35
|
-
h:
|
|
33
|
+
h: Bun.CryptoHasher,
|
|
36
34
|
): Promise<void> {
|
|
37
35
|
let appCss: string;
|
|
38
36
|
try {
|
|
@@ -120,12 +118,15 @@ async function readInstalledPkgVersion(
|
|
|
120
118
|
|
|
121
119
|
export async function hashTeamgbgJsGraph(
|
|
122
120
|
cwd: string,
|
|
123
|
-
h:
|
|
121
|
+
h: Bun.CryptoHasher,
|
|
124
122
|
): Promise<void> {
|
|
125
|
-
const roots = [
|
|
123
|
+
const roots = [];
|
|
124
|
+
for (const p of [
|
|
126
125
|
join(cwd, "src", "entry-client.ts"),
|
|
127
126
|
join(cwd, "src", "page-manifest.ts"),
|
|
128
|
-
]
|
|
127
|
+
]) {
|
|
128
|
+
if (await Bun.file(p).exists()) roots.push(p);
|
|
129
|
+
}
|
|
129
130
|
if (roots.length === 0) return; // backend service — no client bundle to guard
|
|
130
131
|
|
|
131
132
|
const seen = new Set<string>();
|
|
@@ -147,7 +148,7 @@ export async function hashTeamgbgJsGraph(
|
|
|
147
148
|
const nmIdx = file.indexOf("/node_modules/@teamscala/");
|
|
148
149
|
if (nmIdx >= 0) {
|
|
149
150
|
const rel = file.slice(nmIdx + "/node_modules/".length); // @teamscala/pkg/...
|
|
150
|
-
fileHashes.set(rel,
|
|
151
|
+
fileHashes.set(rel, new Bun.CryptoHasher("sha256").update(text).digest("hex"));
|
|
151
152
|
}
|
|
152
153
|
const next: string[] = [];
|
|
153
154
|
for (const m of text.matchAll(JS_IMPORT_SPEC_RE)) {
|
package/src/csrf-protection.ts
CHANGED
|
@@ -32,14 +32,19 @@
|
|
|
32
32
|
|
|
33
33
|
import { getManagedBaseDomain } from "@teamscala/auth/auth-env-resolution";
|
|
34
34
|
import { isTrustedAuthOrigin } from "@teamscala/auth/is-trusted-auth-origin";
|
|
35
|
+
import { createCache } from "@teamscala/cache/create-cache";
|
|
35
36
|
import { csrf } from "hono/csrf";
|
|
36
37
|
import type { Context, MiddlewareHandler } from "hono";
|
|
37
38
|
|
|
38
39
|
const POSITIVE_TTL_MS = 5 * 60_000;
|
|
39
40
|
const NEGATIVE_TTL_MS = 10_000;
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// Positive and negative lookups expire on different clocks; the per-entry
|
|
43
|
+
// ttlMs override on set() carries that, the primitive owns eviction.
|
|
44
|
+
const customDomainCache = createCache<boolean>("service-boot:csrf-custom-domains", {
|
|
45
|
+
ttlMs: POSITIVE_TTL_MS,
|
|
46
|
+
maxSize: 512,
|
|
47
|
+
});
|
|
43
48
|
|
|
44
49
|
function isPlatformOrigin(origin: string, baseDomain: string): boolean {
|
|
45
50
|
let host: string;
|
|
@@ -61,9 +66,8 @@ async function isTrustedCustomOrigin(origin: string): Promise<boolean> {
|
|
|
61
66
|
}
|
|
62
67
|
if (!host) return false;
|
|
63
68
|
|
|
64
|
-
const now = Date.now();
|
|
65
69
|
const cached = customDomainCache.get(host);
|
|
66
|
-
if (cached
|
|
70
|
+
if (cached !== undefined) return cached;
|
|
67
71
|
|
|
68
72
|
const trusted = await (async () => {
|
|
69
73
|
try {
|
|
@@ -81,11 +85,7 @@ async function isTrustedCustomOrigin(origin: string): Promise<boolean> {
|
|
|
81
85
|
}
|
|
82
86
|
})();
|
|
83
87
|
|
|
84
|
-
customDomainCache.set(host,
|
|
85
|
-
trusted,
|
|
86
|
-
expiresAt: now + (trusted ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS),
|
|
87
|
-
});
|
|
88
|
-
if (customDomainCache.size > 512) customDomainCache.clear();
|
|
88
|
+
customDomainCache.set(host, trusted, trusted ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS);
|
|
89
89
|
return trusted;
|
|
90
90
|
}
|
|
91
91
|
|
package/src/discover-blocks.ts
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
24
|
-
import { existsSync } from "node:fs";
|
|
25
24
|
import { join } from "node:path";
|
|
26
25
|
import { getPrisma } from "@teamscala/db/prisma-registry";
|
|
27
26
|
import { BlockSchemaConfigSchema } from "@teamscala/db-validation/registry-schemas/block-schema";
|
|
@@ -56,17 +55,30 @@ export async function discoverBlocks(
|
|
|
56
55
|
// so the 2026-05-29 empty-list-scans-everything incident cannot recur either.
|
|
57
56
|
const serviceModules = join(process.cwd(), "node_modules");
|
|
58
57
|
const installedCache = new Map<string, boolean>();
|
|
59
|
-
const isInstalled = (pkg: string): boolean => {
|
|
58
|
+
const isInstalled = async (pkg: string): Promise<boolean> => {
|
|
60
59
|
const cached = installedCache.get(pkg);
|
|
61
60
|
if (cached !== undefined) return cached;
|
|
62
|
-
|
|
61
|
+
// Probe the package's package.json, NOT its directory. Bun.file() on a
|
|
62
|
+
// DIRECTORY reports exists() === false, so the previous directory probe
|
|
63
|
+
// answered "not installed" for every package that was in fact installed
|
|
64
|
+
// — and because this predicate gates every block below, that skipped
|
|
65
|
+
// every block and booted the service with zero Puck components. Measured
|
|
66
|
+
// 2026-09-01: scala-portal, scala-public, scala-forms and scala-projects
|
|
67
|
+
// all FATAL at ui-service.page-manifest with their packages present on
|
|
68
|
+
// disk at the expected versions. A file probe is the correct shape.
|
|
69
|
+
const ok = await Bun.file(
|
|
70
|
+
join(serviceModules, ...pkg.split("/"), "package.json"),
|
|
71
|
+
).exists();
|
|
63
72
|
installedCache.set(pkg, ok);
|
|
64
73
|
return ok;
|
|
65
74
|
};
|
|
66
75
|
// `blockPackages` is retained only to surface authored-vs-installed drift to
|
|
67
76
|
// the operator; it no longer gates discovery, so it can never blank a block.
|
|
68
77
|
if (blockPackages.length > 0) {
|
|
69
|
-
const
|
|
78
|
+
const installed = await Promise.all(
|
|
79
|
+
blockPackages.map(async (p) => ({ p, ok: await isInstalled(p) })),
|
|
80
|
+
);
|
|
81
|
+
const stale = installed.filter((x) => !x.ok).map((x) => x.p);
|
|
70
82
|
if (stale.length > 0) {
|
|
71
83
|
getAppLogger().warn(
|
|
72
84
|
`[discover-blocks] block_packages lists ${stale.length} package(s) that are not installed (ignored): ${stale.join(", ")}`,
|
|
@@ -105,7 +117,7 @@ export async function discoverBlocks(
|
|
|
105
117
|
const rawPackage = rawComponentPath
|
|
106
118
|
? rawComponentPath.split("/").slice(0, 2).join("/")
|
|
107
119
|
: null;
|
|
108
|
-
if (!rawPackage || !isInstalled(rawPackage)) {
|
|
120
|
+
if (!rawPackage || !(await isInstalled(rawPackage))) {
|
|
109
121
|
skipped.push(
|
|
110
122
|
`${row.slug} (${rawPackage ?? "missing componentPath"} not installed)`,
|
|
111
123
|
);
|
|
@@ -131,7 +143,7 @@ export async function discoverBlocks(
|
|
|
131
143
|
// Scope to installed packages: each block_schema's componentPath must
|
|
132
144
|
// start with a package present in the service's node_modules.
|
|
133
145
|
const pkgPrefix = componentPath.split("/").slice(0, 2).join("/");
|
|
134
|
-
if (!isInstalled(pkgPrefix)) {
|
|
146
|
+
if (!(await isInstalled(pkgPrefix))) {
|
|
135
147
|
skipped.push(`${row.slug} (${pkgPrefix} not installed)`);
|
|
136
148
|
return;
|
|
137
149
|
}
|
|
@@ -176,12 +188,16 @@ export async function discoverBlocks(
|
|
|
176
188
|
// missing componentPath are kept so `loadBlock` still flags them (real data
|
|
177
189
|
// signal); `skipped` thus catches only genuine failures (installed package
|
|
178
190
|
// but import/config/export failed) and the call-site WARN becomes signal.
|
|
179
|
-
const candidateRows =
|
|
191
|
+
const candidateRows = [];
|
|
192
|
+
for (const row of rows) {
|
|
180
193
|
const cp = getStringProperty(parseConfig(row.config), "componentPath");
|
|
181
|
-
if (!cp)
|
|
194
|
+
if (!cp) {
|
|
195
|
+
candidateRows.push(row);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
182
198
|
const pkg = cp.split("/").slice(0, 2).join("/");
|
|
183
|
-
|
|
184
|
-
}
|
|
199
|
+
if (await isInstalled(pkg)) candidateRows.push(row);
|
|
200
|
+
}
|
|
185
201
|
// All blocks concurrent (module imports are in-process I/O + V8 compile — no
|
|
186
202
|
// fork tax, so bounded concurrency isn't needed here; the chunk-of-8 was
|
|
187
203
|
// needlessly serializing 44 imports into 6 waves when 1 wave suffices).
|
|
@@ -14,13 +14,18 @@ import { join } from "node:path";
|
|
|
14
14
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
15
15
|
import type { Hono } from "hono";
|
|
16
16
|
import { etag } from "hono/etag";
|
|
17
|
+
import { FALLBACK_PRIMARY_COLOR } from "@teamscala/os/brand/fallback-primary-color";
|
|
17
18
|
import { buildErrorReport } from "@teamscala/os/errors/error-report";
|
|
18
19
|
import { collectHeapReport } from "@teamscala/os/observability/heap-report";
|
|
19
20
|
import { isLoopbackHost } from "@teamscala/os/observability/loopback-host";
|
|
20
21
|
import { handleAssetProxy } from "./asset-proxy";
|
|
21
22
|
import { resolveBrandFaviconPath, warmBrandFaviconCache } from "./resolve-brand-favicon";
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
// Served ONLY when the org has no brand favicon, so the correct colour is the
|
|
25
|
+
// platform brand default rather than a literal. This changes the generic
|
|
26
|
+
// fallback mark from indigo to the canonical slate; any org with its own
|
|
27
|
+
// favicon is unaffected (resolveBrandFaviconPath wins before this is reached).
|
|
28
|
+
const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="${FALLBACK_PRIMARY_COLOR}"/><text x="16" y="23" text-anchor="middle" font-size="20" font-family="system-ui" fill="white">S</text></svg>`;
|
|
24
29
|
|
|
25
30
|
export async function registerStaticAssets(
|
|
26
31
|
app: Hono,
|