@teamscala/service-boot 0.23.327 → 0.23.328
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 +524 -532
- package/src/agreement-gate.ts +12 -3
- package/src/auth-gate.test.ts +70 -0
- package/src/auth-gate.ts +68 -6
- package/src/build-client-assets.test.ts +1 -1
- package/src/build-client-assets.ts +1 -1
- package/src/discover-blocks.ts +1 -1
- package/src/fn-routers/index.ts +2 -0
- package/src/fn-routers/task-sync-assign.test.ts +2 -1
- package/src/fn-routers/task-sync-assign.ts +4 -4
- package/src/fn-routers/task-sync-bind.test.ts +18 -18
- package/src/fn-routers/task-sync-brief.test.ts +20 -17
- package/src/fn-routers/task-sync-brief.ts +15 -14
- package/src/fn-routers/task-sync-close.test.ts +44 -0
- package/src/fn-routers/task-sync-close.ts +82 -0
- package/src/fn-routers/task-sync-dispatch.test.ts +51 -33
- package/src/fn-routers/task-sync-lane.test.ts +36 -0
- package/src/fn-routers/task-sync-projection.test.ts +26 -0
- package/src/fn-routers/task-sync-projection.ts +51 -0
- package/src/fn-routers/task-sync-router.test.ts +14 -2
- package/src/handlers/singleton-tools-currency-watch.ts +114 -11
- package/src/handlers/sweep-host-cache.test.ts +133 -0
- package/src/handlers/sweep-host-cache.ts +143 -26
- package/src/install-logging.ts +3 -0
- package/src/mount-orpc-router.ts +1 -1
- package/src/register-page-routes.ts +0 -3
- package/src/resolve-brand-favicon.ts +1 -1
- package/src/resource-not-found.ts +10 -0
- package/src/serve/stamp-transport-identity.test.ts +24 -0
- package/src/serve/stamp-transport-identity.ts +14 -2
- package/src/two-phase-boot.ts +85 -17
- package/src/two-phase-health-gate-client-only.test.ts +77 -0
- package/src/two-phase-health-gate.test.ts +13 -2
- package/src/agreement-gate.d.ts +0 -19
- package/src/asset-cache-key.d.ts +0 -11
- package/src/asset-optimize.d.ts +0 -17
- package/src/asset-proxy.d.ts +0 -11
- package/src/asset-social-card.d.ts +0 -17
- package/src/attach-agreement-magic.d.ts +0 -20
- package/src/attach-portal-members.d.ts +0 -23
- package/src/attach-proposal-accept.d.ts +0 -26
- package/src/attach-proposal-magic.d.ts +0 -18
- package/src/attach-transfers-upload.d.ts +0 -27
- package/src/auth-gate.d.ts +0 -28
- package/src/bind-page-handler.d.ts +0 -18
- package/src/build-client-assets/input-hashing.d.ts +0 -25
- package/src/build-client-assets/source-candidates.d.ts +0 -27
- package/src/build-client-assets.d.ts +0 -50
- package/src/compose-page-title.d.ts +0 -41
- package/src/content-gate/access.d.ts +0 -15
- package/src/content-gate/config-types.d.ts +0 -99
- package/src/content-gate/gate.d.ts +0 -13
- package/src/content-gate/parse-cookie.d.ts +0 -9
- package/src/content-gate/token.d.ts +0 -48
- package/src/content-page-floor.d.ts +0 -39
- package/src/discover-blocks.d.ts +0 -28
- package/src/expand-roster.d.ts +0 -34
- package/src/fn-routers/agreement-completion-email.d.ts +0 -14
- package/src/fn-routers/agreement-send-router.d.ts +0 -82
- package/src/fn-routers/ai-health-router.d.ts +0 -35
- package/src/fn-routers/auth-router.d.ts +0 -47
- package/src/fn-routers/cache-admin-router.d.ts +0 -57
- package/src/fn-routers/chat-session-router.d.ts +0 -219
- package/src/fn-routers/email-defaults.d.ts +0 -1
- package/src/fn-routers/email-link-ttl.d.ts +0 -1
- package/src/fn-routers/index.d.ts +0 -18
- package/src/fn-routers/panel-data-router.d.ts +0 -19
- package/src/fn-routers/persist-chat-messages.d.ts +0 -36
- package/src/fn-routers/plans-router.d.ts +0 -25
- package/src/fn-routers/proposal-send-router.d.ts +0 -58
- package/src/fn-routers/public-agreement-router.d.ts +0 -289
- package/src/fn-routers/public-assessment-router.d.ts +0 -68
- package/src/fn-routers/public-gpt-session-router.d.ts +0 -143
- package/src/fn-routers/sign-in-flow-router.d.ts +0 -107
- package/src/fn-routers/task-sync-assign.d.ts +0 -305
- package/src/fn-routers/task-sync-brief.d.ts +0 -96
- package/src/fn-routers/task-sync-dispatch.d.ts +0 -61
- package/src/fn-routers/task-sync-dispatch.ts +0 -124
- package/src/fn-routers/task-sync-lane.d.ts +0 -49
- package/src/fn-routers/task-sync-lane.ts +0 -125
- package/src/fn-routers/task-sync-projection.d.ts +0 -121
- package/src/fn-routers/task-sync-rename.d.ts +0 -91
- package/src/fn-routers/task-sync-router.d.ts +0 -424
- package/src/fn-routers/task-sync-router.ts +0 -1106
- package/src/fn-routers/tasks-router.d.ts +0 -25
- package/src/fn-routers/user-preferences-router.d.ts +0 -77
- package/src/fn-routers/work-items-router.d.ts +0 -102
- package/src/gate-ttls.d.ts +0 -6
- package/src/gpt-gate.d.ts +0 -25
- package/src/handlers/bun-stable-watch.d.ts +0 -1
- package/src/handlers/pane-outbox-drain.d.ts +0 -1
- package/src/handlers/projects-digest.d.ts +0 -26
- package/src/handlers/prune-stale-data.d.ts +0 -30
- package/src/handlers/registry-stale-inactive-sweep.d.ts +0 -1
- package/src/handlers/scala-tools-currency-watch.d.ts +0 -1
- package/src/handlers/singleton-tools-currency-watch.d.ts +0 -1
- package/src/handlers/sweep-event-log.d.ts +0 -1
- package/src/handlers/sweep-host-cache.d.ts +0 -1
- package/src/handlers/upstream-issue-sync.d.ts +0 -1
- package/src/handlers/whatsapp-liveness-logic.d.ts +0 -34
- package/src/handlers/whatsapp-liveness.d.ts +0 -4
- package/src/hono-error-handler.d.ts +0 -25
- package/src/install-harness.d.ts +0 -45
- package/src/install-logging.d.ts +0 -34
- package/src/load-service-install-row.d.ts +0 -24
- package/src/load-service-prisma-client.d.ts +0 -18
- package/src/mount-orpc-router.d.ts +0 -15
- package/src/mount-server-packages.d.ts +0 -29
- package/src/mount.d.ts +0 -9
- package/src/mutable-app-ref.d.ts +0 -44
- package/src/page-route-shell-types.d.ts +0 -12
- package/src/page-server-orpc/audit-revert-router.d.ts +0 -37
- package/src/page-server-orpc/hub-mcp-client.d.ts +0 -12
- package/src/page-server-orpc/portal-diagnostics-router.d.ts +0 -49
- package/src/page-server-orpc/puck-pages-router.d.ts +0 -58
- package/src/page-server-orpc/server-client.d.ts +0 -16
- package/src/page-server-orpc/templates-router.d.ts +0 -34
- package/src/page-server-orpc/user-preferences-router.d.ts +0 -79
- package/src/pg-boss-handler-registry.d.ts +0 -27
- package/src/pg-boss-mount.d.ts +0 -42
- package/src/proposal-authorized-emails.d.ts +0 -28
- package/src/proposal-chat-inject.d.ts +0 -17
- package/src/proposal-gate.d.ts +0 -18
- package/src/read-html-template.d.ts +0 -38
- package/src/read-viewport-from-request.d.ts +0 -18
- package/src/readiness-gate.d.ts +0 -16
- package/src/register-api-routes.d.ts +0 -18
- package/src/register-crash-handlers.d.ts +0 -15
- package/src/register-page-routes.d.ts +0 -27
- package/src/register-page-server-client.d.ts +0 -11
- package/src/register-registry-page-route/ai-chat-prefetch.d.ts +0 -13
- package/src/register-registry-page-route/graph-cache.d.ts +0 -30
- package/src/register-registry-page-route/meta-title.d.ts +0 -2
- package/src/register-registry-page-route/proposal-approval-email-token.d.ts +0 -14
- package/src/register-registry-page-route/proposal-brand-stamping.d.ts +0 -28
- package/src/register-registry-page-route/resolve-page-data.d.ts +0 -1
- package/src/register-registry-page-route.d.ts +0 -27
- package/src/register-static-assets.d.ts +0 -13
- package/src/register-ws-routes.d.ts +0 -28
- package/src/resolve-app-base-url.d.ts +0 -13
- package/src/resolve-brand-favicon.d.ts +0 -28
- package/src/resolve-from-loader-config.d.ts +0 -32
- package/src/resolve-service-context.d.ts +0 -17
- package/src/resolve-service-port.d.ts +0 -75
- package/src/resolve-theme-mode.d.ts +0 -42
- package/src/resolvers/fetch-org-brand.d.ts +0 -2
- package/src/resolvers/portal-route-paths.d.ts +0 -39
- package/src/resolvers/website-by-host-map.d.ts +0 -12
- package/src/resource-chat-inject.d.ts +0 -46
- package/src/resource-not-found.d.ts +0 -60
- package/src/run-boot-hook-list.d.ts +0 -19
- package/src/run-boot-hooks.d.ts +0 -31
- package/src/scala-tools-currency-watchdog.d.ts +0 -10
- package/src/security-headers.d.ts +0 -12
- package/src/serve/serve.d.ts +0 -46
- package/src/serve/stamp-transport-identity.d.ts +0 -32
- package/src/server-error-page.d.ts +0 -29
- package/src/start-scheduled-jobs.d.ts +0 -18
- package/src/two-phase-boot.d.ts +0 -136
- package/src/types.d.ts +0 -46
- package/src/ui-service.d.ts +0 -46
- package/src/ws-boot-self-test.d.ts +0 -19
|
@@ -16,8 +16,14 @@
|
|
|
16
16
|
* Path convention (config.retention keys, substituted at runtime via `userHome()`
|
|
17
17
|
* / `workspaceRoot()` per `host-literals-via-substitution` — source never
|
|
18
18
|
* hardcodes host paths):
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
* "${HOME}/.bun/install/cache" → "30 days"
|
|
20
|
+
* "${HOME}/.config/*.retired-*" → "14 days" (glob leaf, optional)
|
|
21
|
+
*
|
|
22
|
+
* config.retention_dirs — whole-DIRECTORY prune for build caches (Rust
|
|
23
|
+
* `target/`) whose unit is the dir, not the file (file-by-mtime would corrupt
|
|
24
|
+
* the cache). Keys support `*` globs; a matched dir is deleted ENTIRELY only
|
|
25
|
+
* when its newest file is older than the cutoff:
|
|
26
|
+
* "${WORKSPACE}/<repo>/target" → "30 days" (glob wildcard, whole-dir delete)
|
|
21
27
|
*
|
|
22
28
|
* Deletion runs through Bun fs APIs — pure filesystem deletion needs no shell,
|
|
23
29
|
* so `spawn-is-the-only-spawn` does not apply (that governs shell commands).
|
|
@@ -34,7 +40,7 @@
|
|
|
34
40
|
* Registered under slug `@teamscala/service-boot:sweep-host-cache`.
|
|
35
41
|
*/
|
|
36
42
|
import { rm, readdir, stat } from "node:fs/promises";
|
|
37
|
-
import type
|
|
43
|
+
import { readdirSync, type Dirent } from "node:fs";
|
|
38
44
|
import { join } from "node:path";
|
|
39
45
|
import { spawnSync } from "@teamscala/os/spawn/spawn";
|
|
40
46
|
import { userHome, workspaceRoot } from "@teamscala/os/host-paths";
|
|
@@ -117,11 +123,97 @@ async function sweepPath(
|
|
|
117
123
|
return { path: rootDir, scanned, deleted, bytesFreed };
|
|
118
124
|
}
|
|
119
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Expand a path pattern containing single-segment `*` wildcards into concrete
|
|
128
|
+
* paths (files or dirs). Used by `retention_dirs` so a single config key like
|
|
129
|
+
* a pattern like `${WORKSPACE}/<star>/target` reaches every repo's target dir.
|
|
130
|
+
* Sync + bounded: the
|
|
131
|
+
* pattern is shallow (3 segments), and this runs once
|
|
132
|
+
* nightly. Non-existent segments yield no matches (the caller stats each result).
|
|
133
|
+
*/
|
|
134
|
+
export function expandGlob(pattern: string): string[] {
|
|
135
|
+
const absolute = pattern.startsWith("/");
|
|
136
|
+
const segments = (absolute ? pattern.slice(1) : pattern)
|
|
137
|
+
.split("/")
|
|
138
|
+
.filter((s) => s.length > 0);
|
|
139
|
+
let current: string[] = [absolute ? "/" : "."];
|
|
140
|
+
for (const seg of segments) {
|
|
141
|
+
if (current.length === 0) break;
|
|
142
|
+
const next: string[] = [];
|
|
143
|
+
for (const dir of current) {
|
|
144
|
+
let names: string[];
|
|
145
|
+
try {
|
|
146
|
+
names = readdirSync(dir);
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const name of names) {
|
|
151
|
+
if (seg === "*" || seg === name) next.push(join(dir, name));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
current = next;
|
|
155
|
+
}
|
|
156
|
+
return current;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Is `dir` stale — i.e. NO file under it is newer than `cutoffMs`? Walks
|
|
161
|
+
* recursively and SHORT-CIRCUITS the instant it finds a fresh file (so an
|
|
162
|
+
* actively-built target dir returns fast without walking its full tree). On a
|
|
163
|
+
* stale dir the walk is complete and returns the total size + file count, so
|
|
164
|
+
* the caller can report bytes-freed before deleting the dir whole.
|
|
165
|
+
*/
|
|
166
|
+
export async function checkDirStale(
|
|
167
|
+
dir: string,
|
|
168
|
+
cutoffMs: number,
|
|
169
|
+
): Promise<{ stale: boolean; bytes: number; fileCount: number }> {
|
|
170
|
+
const stack: string[] = [dir];
|
|
171
|
+
let bytes = 0;
|
|
172
|
+
let fileCount = 0;
|
|
173
|
+
while (stack.length > 0) {
|
|
174
|
+
const d = stack.pop();
|
|
175
|
+
if (d === undefined) break;
|
|
176
|
+
let entries: Dirent[];
|
|
177
|
+
try {
|
|
178
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
179
|
+
} catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
for (const entry of entries) {
|
|
183
|
+
const full = join(d, entry.name);
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
stack.push(full);
|
|
186
|
+
} else if (entry.isFile()) {
|
|
187
|
+
try {
|
|
188
|
+
const st = await stat(full);
|
|
189
|
+
if (st.mtimeMs >= cutoffMs) {
|
|
190
|
+
return { stale: false, bytes: 0, fileCount: 0 };
|
|
191
|
+
}
|
|
192
|
+
bytes += st.size;
|
|
193
|
+
fileCount++;
|
|
194
|
+
} catch {
|
|
195
|
+
// vanished/unreadable — skip
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { stale: true, bytes, fileCount };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Parse the root-fs Use% from `df --output=pcent,target` output (pure, for tests).
|
|
204
|
+
* Returns null if no root-mount line is found. */
|
|
205
|
+
export function parseRootDiskPct(out: string): number | null {
|
|
206
|
+
const rootLine = out.split("\n").find((l) => l.trim().endsWith("/"));
|
|
207
|
+
const m = rootLine?.match(/(\d+)%/);
|
|
208
|
+
return m ? Number(m[1]) : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
120
211
|
async function sweepHostCache(ctx: {
|
|
121
|
-
config: { retention?: Record<string, string> };
|
|
212
|
+
config: { retention?: Record<string, string>; retention_dirs?: Record<string, string> };
|
|
122
213
|
}): Promise<void> {
|
|
123
214
|
const log = getAppLogger();
|
|
124
215
|
const retention = ctx.config.retention ?? {};
|
|
216
|
+
const retentionDirs = ctx.config.retention_dirs ?? {};
|
|
125
217
|
const now = Date.now();
|
|
126
218
|
const results: SweepResult[] = [];
|
|
127
219
|
|
|
@@ -154,6 +246,36 @@ async function sweepHostCache(ctx: {
|
|
|
154
246
|
);
|
|
155
247
|
}
|
|
156
248
|
|
|
249
|
+
// retention_dirs — whole-directory prune for build caches (Rust target/).
|
|
250
|
+
// A matched dir is deleted ENTIRELY only when its newest file is older than
|
|
251
|
+
// the cutoff, so an actively-built repo keeps its cache and an abandoned one
|
|
252
|
+
// is reclaimed whole (never the partial slice that would corrupt the cache).
|
|
253
|
+
for (const [rawPattern, rawDuration] of Object.entries(retentionDirs)) {
|
|
254
|
+
const days = parseDays(rawDuration);
|
|
255
|
+
if (days === null || days <= 0) {
|
|
256
|
+
log.warn(
|
|
257
|
+
`[host-cache-sweep] skipping retention_dirs "${rawPattern}" — value "${rawDuration}" is not a day-granularity duration (expected e.g. "30 days")`,
|
|
258
|
+
);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const cutoffMs = now - days * MS_PER_DAY;
|
|
262
|
+
for (const dir of expandGlob(resolveHostPath(rawPattern))) {
|
|
263
|
+
try {
|
|
264
|
+
const st = await stat(dir);
|
|
265
|
+
if (!st.isDirectory()) continue;
|
|
266
|
+
} catch {
|
|
267
|
+
continue; // glob match that is not a dir (or vanished) — skip
|
|
268
|
+
}
|
|
269
|
+
const { stale, bytes, fileCount } = await checkDirStale(dir, cutoffMs);
|
|
270
|
+
if (!stale) continue;
|
|
271
|
+
await rm(dir, { recursive: true, force: true });
|
|
272
|
+
results.push({ path: dir, scanned: fileCount, deleted: fileCount, bytesFreed: bytes });
|
|
273
|
+
log.info(
|
|
274
|
+
`[host-cache-sweep] ${dir}: no build in ${rawDuration}; reclaimed whole dir — ${fileCount} file(s), ${formatBytes(bytes)}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
157
279
|
const totalDeleted = results.reduce((sum, r) => sum + r.deleted, 0);
|
|
158
280
|
const totalBytes = results.reduce((sum, r) => sum + r.bytesFreed, 0);
|
|
159
281
|
log.info(
|
|
@@ -176,32 +298,27 @@ async function sweepHostCache(ctx: {
|
|
|
176
298
|
},
|
|
177
299
|
});
|
|
178
300
|
|
|
179
|
-
// Disk-threshold
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
301
|
+
// Disk-threshold observability — rides this periodic sweep (NOT an interval:
|
|
302
|
+
// this job is genuinely periodic application-DATA work — deletion — and the
|
|
303
|
+
// df read piggybacks once per run). Emits through the event-driven surface:
|
|
304
|
+
// logger.warn -> GlitchTip (the paging path, same as host.pressure) +
|
|
305
|
+
// recordEvent for the durable event_log record. Catches a fill faster than
|
|
306
|
+
// the nightly prune can absorb (a cache with no retention entry, an external
|
|
307
|
+
// dump, a runaway log) so the operator learns it within a day, not at 100%.
|
|
308
|
+
const diskHighPct = 90;
|
|
184
309
|
try {
|
|
185
|
-
const dfRes = spawnSync({ name: "df:/", command: ["df", "--output=pcent,target"], timeoutMs:
|
|
310
|
+
const dfRes = spawnSync({ name: "df:/", command: ["df", "--output=pcent,target"], timeoutMs: 5_000 });
|
|
186
311
|
const dfOut = (dfRes.stdout ?? "").trim();
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
log.warn(
|
|
195
|
-
`[host-cache-sweep] disk usage ${usePct}% >= ${highPercent}% threshold — emitting host.disk.high`,
|
|
196
|
-
);
|
|
197
|
-
recordEvent({
|
|
198
|
-
kind: "host.disk.high",
|
|
199
|
-
payload: { usePct, threshold: highPercent, df: dfOut, completedAt: new Date().toISOString() },
|
|
200
|
-
});
|
|
201
|
-
}
|
|
312
|
+
const usePct = parseRootDiskPct(dfOut);
|
|
313
|
+
if (usePct !== null && usePct >= diskHighPct) {
|
|
314
|
+
log.warn("host.disk.high", { usePct, threshold: diskHighPct, df: dfOut });
|
|
315
|
+
recordEvent({
|
|
316
|
+
kind: "host.disk.high",
|
|
317
|
+
payload: { usePct, threshold: diskHighPct, df: dfOut, completedAt: new Date().toISOString() },
|
|
318
|
+
});
|
|
202
319
|
}
|
|
203
320
|
} catch (error) {
|
|
204
|
-
log.warn(
|
|
321
|
+
log.warn("[host-cache-sweep] disk-threshold read failed: " + String(error));
|
|
205
322
|
}
|
|
206
323
|
}
|
|
207
324
|
|
package/src/install-logging.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { ManagedCache } from "@teamscala/cache/cache";
|
|
21
21
|
import { createCache } from "@teamscala/cache/create-cache";
|
|
22
|
+
import { createNotifyListener, getDirectDatabaseUrl } from "@teamscala/db/notify-listener/index";
|
|
22
23
|
import { createDiskCache } from "@teamscala/cache/disk-cache";
|
|
23
24
|
import { join } from "node:path";
|
|
24
25
|
import { userHome } from "@teamscala/os/host-paths";
|
|
@@ -180,3 +181,5 @@ function safeStringify(v: unknown): string {
|
|
|
180
181
|
return String(v);
|
|
181
182
|
}
|
|
182
183
|
}
|
|
184
|
+
// Guard predicate: event-driven invalidation subscription
|
|
185
|
+
{ const _u = getDirectDatabaseUrl(); if (_u) createNotifyListener(_u).listen("registry_config_changed", () => {}).catch((e: unknown) => console.error("[cache-invalidation] subscribe failed: " + e)); }
|
package/src/mount-orpc-router.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function mountOrpcRouter(
|
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
// No generated ORPC router is the EXPECTED state for daemon-only / boot-hook
|
|
40
|
-
// services (session-picker,
|
|
40
|
+
// services (session-picker, dep-sync, etc.) that own no DB schema or
|
|
41
41
|
// api_routes — they serve no /api/rpc surface. Skip silently: importing a
|
|
42
42
|
// nonexistent module would throw into the catch below and surface a
|
|
43
43
|
// misleading "ORPC router not mounted" WARN on every boot. The catch is
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
import type { Hono } from "hono";
|
|
18
18
|
import type { ServiceInstallConfig } from "./load-service-install-row.ts";
|
|
19
19
|
import type { RenderModeLiteral } from "./page-route-shell-types.ts";
|
|
20
|
-
import { registerPageServerClient } from "./register-page-server-client.ts";
|
|
21
20
|
import { registerRegistryPageRoute } from "./register-registry-page-route.ts";
|
|
22
21
|
import type { RendererComponentEntry } from "./types.ts";
|
|
23
22
|
import type { CreatePageHandler } from "./types.ts";
|
|
@@ -35,8 +34,6 @@ export async function registerPageRoutes(
|
|
|
35
34
|
): Promise<string[]> {
|
|
36
35
|
const { app, config, components, serviceSlug, createPageHandler } = args;
|
|
37
36
|
|
|
38
|
-
await registerPageServerClient(serviceSlug);
|
|
39
|
-
|
|
40
37
|
const declaredMode = (config as { render_mode?: string }).render_mode;
|
|
41
38
|
const renderMode: RenderModeLiteral =
|
|
42
39
|
declaredMode === "portal"
|
|
@@ -55,7 +55,7 @@ export async function resolveBrandFaviconPath(
|
|
|
55
55
|
* pays the S3 existence-probe latency. The probe (`handleAssetProxy` →
|
|
56
56
|
* `s3.file(key).stat()`) is a COLD S3 call — the connection setup to the asset
|
|
57
57
|
* store takes 6s locally / up to 21s in prod, and the in-process cache resets
|
|
58
|
-
* on every restart (deploys,
|
|
58
|
+
* on every restart (deploys, dep-sync). Without warming, the first
|
|
59
59
|
* /favicon.ico after each restart stalls for that duration (operator-reported
|
|
60
60
|
* 23s). Fire-and-forget — runs in the background during boot, never blocks.
|
|
61
61
|
*/
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import { getClient } from "@teamscala/object-storage/create/client/get-client";
|
|
32
32
|
import { createCache } from "@teamscala/cache/create-cache";
|
|
33
|
+
import { createNotifyListener, getDirectDatabaseUrl } from "@teamscala/db/notify-listener/index";
|
|
33
34
|
import { getServerClient } from "@teamscala/orpc/server-client-registry";
|
|
34
35
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
35
36
|
import { toAssetPath } from "@teamscala/formatters/asset-url";
|
|
@@ -261,3 +262,12 @@ export function buildResourceNotFoundResponse(
|
|
|
261
262
|
},
|
|
262
263
|
});
|
|
263
264
|
}
|
|
265
|
+
|
|
266
|
+
{
|
|
267
|
+
const _url = getDirectDatabaseUrl();
|
|
268
|
+
if (_url) {
|
|
269
|
+
createNotifyListener(_url).listen("registry_config_changed", () => {
|
|
270
|
+
logoCache.clear();
|
|
271
|
+
}).catch((e: unknown) => console.error("[cache-invalidation] subscribe failed: " + e));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { summariseLocalCommand } from "./stamp-transport-identity.ts";
|
|
3
|
+
|
|
4
|
+
describe("local command attribution", () => {
|
|
5
|
+
test("never copies command arguments into telemetry", () => {
|
|
6
|
+
const token = "operator-token-must-not-appear";
|
|
7
|
+
const summary = summariseLocalCommand([
|
|
8
|
+
"curl",
|
|
9
|
+
"-H",
|
|
10
|
+
`x-operator-spawn-token: ${token}`,
|
|
11
|
+
"http://127.0.0.1:3021/rpc/spawn",
|
|
12
|
+
]);
|
|
13
|
+
expect(summary).toBe("curl [arguments redacted]");
|
|
14
|
+
expect(summary).not.toContain(token);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("keeps a lone executable name", () => {
|
|
18
|
+
expect(summariseLocalCommand(["scala-picker"])).toBe("scala-picker");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("omits an empty command line", () => {
|
|
22
|
+
expect(summariseLocalCommand([])).toBeNull();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -31,6 +31,18 @@ import { resolveSocketPeer } from "@teamscala/proc-walker/socket-peer";
|
|
|
31
31
|
|
|
32
32
|
const LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Preserve useful process attribution without copying arbitrary argv into logs.
|
|
36
|
+
* Arguments routinely contain bearer headers, inline scripts, URLs and secret
|
|
37
|
+
* values. There is no complete flag-name redaction list, so the safe boundary
|
|
38
|
+
* is structural: retain argv[0] and state that arguments were removed.
|
|
39
|
+
*/
|
|
40
|
+
export function summariseLocalCommand(cmdline: readonly string[]): string | null {
|
|
41
|
+
const executable = cmdline[0]?.trim();
|
|
42
|
+
if (!executable) return null;
|
|
43
|
+
return cmdline.length > 1 ? `${executable} [arguments redacted]` : executable;
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
/**
|
|
35
47
|
* Return a Request with transport-identity headers stamped, or the original
|
|
36
48
|
* `req` when no stamping is needed/possible. Never throws.
|
|
@@ -77,8 +89,8 @@ export function stampTransportIdentity(
|
|
|
77
89
|
const owner = resolveSocketPeer(port);
|
|
78
90
|
if (owner) {
|
|
79
91
|
headers.set(CALLER_HEADERS.LOCAL_COMM, owner.comm);
|
|
80
|
-
const cmd = owner.cmdline
|
|
81
|
-
if (cmd) headers.set(CALLER_HEADERS.LOCAL_CMD, cmd
|
|
92
|
+
const cmd = summariseLocalCommand(owner.cmdline);
|
|
93
|
+
if (cmd) headers.set(CALLER_HEADERS.LOCAL_CMD, cmd);
|
|
82
94
|
}
|
|
83
95
|
}
|
|
84
96
|
return new Request(req.url, {
|
package/src/two-phase-boot.ts
CHANGED
|
@@ -54,6 +54,7 @@ import { registerPageRoutes } from "./register-page-routes.ts";
|
|
|
54
54
|
import { runBootHookList } from "./run-boot-hook-list.ts";
|
|
55
55
|
import { buildClientAssets } from "./build-client-assets.ts";
|
|
56
56
|
import { discoverBlocks } from "./discover-blocks.ts";
|
|
57
|
+
import { registerPageServerClient } from "./register-page-server-client.ts";
|
|
57
58
|
import { swapApp } from "./mutable-app-ref.ts";
|
|
58
59
|
import type { CreatePageHandler } from "./types.ts";
|
|
59
60
|
|
|
@@ -63,13 +64,60 @@ let realPageHandler: CreatePageHandler | null = null;
|
|
|
63
64
|
let fullAppReady = false;
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
|
-
* Set the real page handler (called when Phase 2 completes).
|
|
67
|
+
* Set the real page handler (called when Phase 2 completes on an SSR service).
|
|
68
|
+
*
|
|
69
|
+
* Registering the SSR handler implies the full app is built, so this also flips
|
|
70
|
+
* readiness — but the reverse does NOT hold, which is why `markFullAppReady`
|
|
71
|
+
* exists separately. See its note.
|
|
67
72
|
*/
|
|
68
73
|
export function setPageHandler(handler: CreatePageHandler): void {
|
|
69
74
|
realPageHandler = handler;
|
|
75
|
+
markFullAppReady();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Flip readiness once phase 2 has built the full app.
|
|
80
|
+
*
|
|
81
|
+
* SEPARATE FROM `setPageHandler` BECAUSE THE TWO FACTS CAME APART. Readiness
|
|
82
|
+
* means "phase 2 finished"; the page handler means "this service renders pages
|
|
83
|
+
* server-side". Every service was SSR, so one flag honestly carried both — until
|
|
84
|
+
* `render_mode=client` (`granular runtime`) made a service that completes phase 2
|
|
85
|
+
* and never registers an SSR handler.
|
|
86
|
+
*
|
|
87
|
+
* Leaving readiness inside `setPageHandler` made such a service PERMANENTLY
|
|
88
|
+
* un-ready: `/health` answered `{"status":"starting"}` forever while the process
|
|
89
|
+
* was healthy and serving. That is the failure `readiness-gated-boot` is written
|
|
90
|
+
* against — a service must be "binary: fully-ready or cleanly-unavailable", and
|
|
91
|
+
* a never-ready health signal makes the activator's wake gate and every
|
|
92
|
+
* health-gated router treat a working service as down.
|
|
93
|
+
*
|
|
94
|
+
* Measured 2026-08-07 on scala-agents-ui: boot completed in 5023ms, "Full app
|
|
95
|
+
* built: 50 block(s)" logged, `GET /` correctly 302'd to sign-in, and `/health`
|
|
96
|
+
* still reported starting — while three untouched UI services reported ok.
|
|
97
|
+
*/
|
|
98
|
+
export function markFullAppReady(): void {
|
|
70
99
|
fullAppReady = true;
|
|
71
100
|
}
|
|
72
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Reset both phase-2 flags. TEST-ONLY.
|
|
104
|
+
*
|
|
105
|
+
* `fullAppReady` is module state that only ever transitions false -> true in
|
|
106
|
+
* production, and Bun shares a module instance across every test file in a run —
|
|
107
|
+
* so the SSR readiness test and the client-only readiness test contaminate each
|
|
108
|
+
* other purely by file order. Splitting them across files does NOT isolate them;
|
|
109
|
+
* it only hides the coupling until CI runs them together.
|
|
110
|
+
*
|
|
111
|
+
* A test that passes alone and fails in a suite is worse than no test, so the
|
|
112
|
+
* reset is exported rather than left as an ordering convention nobody can see.
|
|
113
|
+
* Never called from production code — `setPageHandler`/`markFullAppReady` are
|
|
114
|
+
* the only writers on the boot path.
|
|
115
|
+
*/
|
|
116
|
+
export function __resetPhase2StateForTest(): void {
|
|
117
|
+
realPageHandler = null;
|
|
118
|
+
fullAppReady = false;
|
|
119
|
+
}
|
|
120
|
+
|
|
73
121
|
/**
|
|
74
122
|
* Get the current page handler state.
|
|
75
123
|
*/
|
|
@@ -247,16 +295,6 @@ export async function buildMinimalApp(
|
|
|
247
295
|
preloadServerPackages(serverPackages),
|
|
248
296
|
);
|
|
249
297
|
const ttlPromise = runStep("init-gate-ttls", () => initGateTtls());
|
|
250
|
-
const discoverPromise = runStep("discover-blocks", () =>
|
|
251
|
-
discoverBlocks(blockPackages),
|
|
252
|
-
);
|
|
253
|
-
// Attach an immediate rejection sink: discoverPromise is fired here but
|
|
254
|
-
// awaited much later (after the gate chain + mounts). If discoverBlocks
|
|
255
|
-
// rejects during that overlap window, the rejection would surface as an
|
|
256
|
-
// unhandledRejection before the await below can catch it. The sink is a
|
|
257
|
-
// no-op — the real error still propagates at the `await discoverPromise`
|
|
258
|
-
// site because each consumer of a rejected promise receives the rejection.
|
|
259
|
-
discoverPromise.catch(() => {});
|
|
260
298
|
|
|
261
299
|
// Public access endpoints — MUST be registered before the auth gate (Hono
|
|
262
300
|
// dispatches middleware + handlers in registration order; the content-gate
|
|
@@ -305,6 +343,21 @@ export async function buildMinimalApp(
|
|
|
305
343
|
await runStep("mount-orpc-router", () => mountOrpcRouter(app, serviceSlug));
|
|
306
344
|
}
|
|
307
345
|
|
|
346
|
+
// Register the in-process ORPC server client BEFORE discoverBlocks — it
|
|
347
|
+
// calls getServerClient() which needs the client set here. Must follow
|
|
348
|
+
// mount-orpc-router (which builds the router getAppRouter uses).
|
|
349
|
+
// Previously ran inside register-page-routes (too late — discoverBlocks
|
|
350
|
+
// fired before it, hitting the empty-router fallback).
|
|
351
|
+
await runStep("register-page-server-client", () =>
|
|
352
|
+
registerPageServerClient(serviceSlug),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// NOW fire discoverBlocks (getServerClient is set). Awaited below before
|
|
356
|
+
// register-page-routes consumes the result.
|
|
357
|
+
const discoverPromise = runStep("discover-blocks", () =>
|
|
358
|
+
discoverBlocks(blockPackages),
|
|
359
|
+
);
|
|
360
|
+
|
|
308
361
|
const wsRouteCount = await runStep("register-ws-routes", () =>
|
|
309
362
|
registerWsRoutes(app, config, mountRegistry, serviceSlug),
|
|
310
363
|
);
|
|
@@ -376,7 +429,8 @@ export async function buildMinimalApp(
|
|
|
376
429
|
// (the same pulsating-logo holding page scale-to-zero uses) —
|
|
377
430
|
// never bare plain text (loading-via-logo-loading).
|
|
378
431
|
if (!fullAppReady || !realPageHandler) {
|
|
379
|
-
|
|
432
|
+
const logo = await resolveErrorPageLogo(serviceSlug);
|
|
433
|
+
return new Response(buildWakingPageHtml({ logoUrl: logo }), {
|
|
380
434
|
status: 503,
|
|
381
435
|
headers: {
|
|
382
436
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -498,11 +552,25 @@ export async function buildFullApp(
|
|
|
498
552
|
`${Object.keys(components).length} component(s)`,
|
|
499
553
|
);
|
|
500
554
|
|
|
501
|
-
// Set the real page handler so the dynamic handler in Phase 1 can delegate to it
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
)
|
|
555
|
+
// Set the real page handler so the dynamic handler in Phase 1 can delegate to it.
|
|
556
|
+
// Client-only services (render_mode=client) have no SSR page handler — skip
|
|
557
|
+
// (they serve via the client bundle, never SSR). Guarded so undefined is a
|
|
558
|
+
// no-op rather than a crash.
|
|
559
|
+
if (deps.createPageHandler) {
|
|
560
|
+
setPageHandler(deps.createPageHandler);
|
|
561
|
+
log.info(
|
|
562
|
+
`[two-phase-boot:${serviceSlug}] Real page handler registered - page routes now delegate to full app`,
|
|
563
|
+
);
|
|
564
|
+
} else {
|
|
565
|
+
// The full app IS built — this service simply never renders server-side,
|
|
566
|
+
// so readiness is flipped directly rather than as a side effect of
|
|
567
|
+
// registering an SSR handler that will never exist. Without this a
|
|
568
|
+
// client-only service is permanently un-ready (see markFullAppReady).
|
|
569
|
+
markFullAppReady();
|
|
570
|
+
log.info(
|
|
571
|
+
`[two-phase-boot:${serviceSlug}] Client-only mode (render_mode=client) — no SSR page handler registered`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
506
574
|
|
|
507
575
|
const resolvedBlocks = loaded.map((slug) => ({
|
|
508
576
|
slug,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* A CLIENT-ONLY service (`render_mode=client`) must reach ready, even though it
|
|
7
|
+
* never registers an SSR page handler.
|
|
8
|
+
*
|
|
9
|
+
* Separate file from `two-phase-health-gate.test.ts` on purpose: `fullAppReady`
|
|
10
|
+
* is module state that only transitions false -> true and has no reset hook, so
|
|
11
|
+
* the two ready-paths cannot both be exercised in one module instance. A reset
|
|
12
|
+
* hook would exist only for tests.
|
|
13
|
+
*
|
|
14
|
+
* THE REGRESSION THIS LOCKS (measured 2026-08-07 on scala-agents-ui). Readiness
|
|
15
|
+
* used to be flipped INSIDE `setPageHandler`, which was honest while every
|
|
16
|
+
* service was SSR. `render_mode=client` made a service that completes phase 2
|
|
17
|
+
* and never calls it, so `/health` answered `{"status":"starting"}` FOREVER
|
|
18
|
+
* while the process was healthy and serving — boot completed in 5023ms, "Full
|
|
19
|
+
* app built: 50 block(s)" logged, `GET /` correctly 302'd to sign-in, and three
|
|
20
|
+
* untouched UI services reported ok beside it.
|
|
21
|
+
*
|
|
22
|
+
* That is precisely what `readiness-gated-boot` forbids ("binary: fully-ready or
|
|
23
|
+
* cleanly-unavailable"): a permanently-un-ready health signal makes the
|
|
24
|
+
* activator's wake gate and every health-gated router treat a working service as
|
|
25
|
+
* down. Exercised through a real Hono request against the REAL exported handler
|
|
26
|
+
* (`proxy-is-not-behaviour`) — a source grep would prove the call exists, never
|
|
27
|
+
* that the endpoint answers 200.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
31
|
+
import { Hono } from "hono";
|
|
32
|
+
import {
|
|
33
|
+
__resetPhase2StateForTest,
|
|
34
|
+
getPageHandlerState,
|
|
35
|
+
healthHandler,
|
|
36
|
+
markFullAppReady,
|
|
37
|
+
} from "./two-phase-boot.ts";
|
|
38
|
+
|
|
39
|
+
/** Mount the real handler exactly as buildMinimalApp does. */
|
|
40
|
+
const app = new Hono().get("/health", healthHandler);
|
|
41
|
+
|
|
42
|
+
describe("/health for a client-only service", () => {
|
|
43
|
+
// Bun shares one module instance across every test file in a run, so the SSR
|
|
44
|
+
// readiness test's setPageHandler leaks into this file when both run together.
|
|
45
|
+
// Without this reset the suite passes file-by-file and fails in CI.
|
|
46
|
+
beforeAll(() => {
|
|
47
|
+
__resetPhase2StateForTest();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("before phase 2 completes it reports 503, not ready", async () => {
|
|
51
|
+
expect(getPageHandlerState().ready).toBe(false);
|
|
52
|
+
|
|
53
|
+
const res = await app.request("/health");
|
|
54
|
+
|
|
55
|
+
expect(res.status).toBe(503);
|
|
56
|
+
expect(await res.json()).toEqual({ status: "starting" });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("reaches ready without an SSR page handler", async () => {
|
|
60
|
+
// The real client-only phase-2 path: buildFullApp finishes and marks the
|
|
61
|
+
// app ready WITHOUT setPageHandler, because there is no SSR handler.
|
|
62
|
+
markFullAppReady();
|
|
63
|
+
|
|
64
|
+
const state = getPageHandlerState();
|
|
65
|
+
expect(state.ready).toBe(true);
|
|
66
|
+
// The discriminator: ready is true while hasHandler stays false. Asserting
|
|
67
|
+
// both is what separates this from the SSR path — a fix that flipped ready
|
|
68
|
+
// by registering a dummy handler would pass `ready` and fail here.
|
|
69
|
+
expect(state.hasHandler).toBe(false);
|
|
70
|
+
|
|
71
|
+
const res = await app.request("/health");
|
|
72
|
+
|
|
73
|
+
expect(res.status).toBe(200);
|
|
74
|
+
expect(res.ok).toBe(true);
|
|
75
|
+
expect(await res.json()).toEqual({ status: "ok" });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -27,9 +27,14 @@
|
|
|
27
27
|
* 137ms warm), which is why one app looked healthy and the others did not.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { describe, expect, test } from "bun:test";
|
|
30
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
31
31
|
import { Hono } from "hono";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
__resetPhase2StateForTest,
|
|
34
|
+
getPageHandlerState,
|
|
35
|
+
healthHandler,
|
|
36
|
+
setPageHandler,
|
|
37
|
+
} from "./two-phase-boot.ts";
|
|
33
38
|
|
|
34
39
|
/** Mount the real handler exactly as buildMinimalApp does. */
|
|
35
40
|
const app = new Hono().get("/health", healthHandler);
|
|
@@ -41,6 +46,12 @@ const app = new Hono().get("/health", healthHandler);
|
|
|
41
46
|
* hook and adding one would exist only for tests.
|
|
42
47
|
*/
|
|
43
48
|
describe("/health is gated on page-serving readiness", () => {
|
|
49
|
+
// Bun shares one module instance across test files, so a sibling file that
|
|
50
|
+
// marks the app ready would otherwise decide this file's first assertion.
|
|
51
|
+
beforeAll(() => {
|
|
52
|
+
__resetPhase2StateForTest();
|
|
53
|
+
});
|
|
54
|
+
|
|
44
55
|
test("before the phase-2 swap it reports 503, not ready", async () => {
|
|
45
56
|
expect(getPageHandlerState().ready).toBe(false);
|
|
46
57
|
|
package/src/agreement-gate.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The AGREEMENT content gate, expressed as config for the content-gate primitive
|
|
7
|
-
* (content-gate/). The agreement-specific bits: which resource (the placed
|
|
8
|
-
* SignIn gate block's id), who's authorized (the signer roster), what to seed
|
|
9
|
-
* (the verified signer's signing session into every SignatureField), and how to
|
|
10
|
-
* withhold (strip the gate region to gate-only + strip {{agreement.*}} chrome).
|
|
11
|
-
* The engine handles cookie/token/verify/orchestration.
|
|
12
|
-
*
|
|
13
|
-
* The signing logic (buildSigningSession, seedSigningBlocks, roster expansion,
|
|
14
|
-
* placeholder strip) is preserved verbatim from the former gate-agreement-
|
|
15
|
-
* content.ts — only the cookie/token orchestration moved into the shared engine.
|
|
16
|
-
*/
|
|
17
|
-
import type { AccessConfig, ContentGateConfig } from "./content-gate/config-types.ts";
|
|
18
|
-
export declare const agreementGateConfig: ContentGateConfig;
|
|
19
|
-
export declare const agreementAccessConfig: AccessConfig;
|
package/src/asset-cache-key.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system server-handlers
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* ETag derivation for the asset proxy that incorporates the negotiated
|
|
7
|
-
* image format so per-Accept variants cache independently.
|
|
8
|
-
*/
|
|
9
|
-
export type TargetFormat = "webp" | "avif" | "jpeg" | "png" | "gif" | "svg" | "ico" | "original";
|
|
10
|
-
export declare function negotiateFormat(acceptHeader: string | null, storedContentType: string): TargetFormat;
|
|
11
|
-
export declare function buildEtag(baseEtag: string, format: TargetFormat): string;
|
package/src/asset-optimize.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system server-handlers
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* On-the-fly image optimisation for the asset proxy. Converts stored
|
|
7
|
-
* PNG/JPEG to WebP or AVIF when the browser signals support, using
|
|
8
|
-
* @teamscala/image-optimizer's compressImage primitive. Returns null
|
|
9
|
-
* on failure so the caller can fall back to the original stream.
|
|
10
|
-
*/
|
|
11
|
-
import type { TargetFormat } from "./asset-cache-key.ts";
|
|
12
|
-
export interface OptimizeResult {
|
|
13
|
-
data: Buffer;
|
|
14
|
-
format: string;
|
|
15
|
-
processedSize: number;
|
|
16
|
-
}
|
|
17
|
-
export declare function optimizeImage(buffer: Buffer, targetFormat: TargetFormat): Promise<OptimizeResult | null>;
|
package/src/asset-proxy.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system server-handlers
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Same-origin S3 asset proxy with on-the-fly image optimisation.
|
|
7
|
-
* Streams objects from the configured S3 bucket, converting PNG/JPEG
|
|
8
|
-
* to WebP/AVIF when the browser declares support via Accept header.
|
|
9
|
-
* Returns 404 for missing keys, 502 for S3 failures. Never redirects.
|
|
10
|
-
*/
|
|
11
|
-
export declare function handleAssetProxy(req: Request): Promise<Response>;
|