@teamscala/service-boot 0.23.478 → 0.23.481
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 +1 -1
- package/src/fn-routers/assessment-report-pair.test.ts +29 -0
- package/src/fn-routers/assessment-research-report-data.test.ts +1 -30
- package/src/fn-routers/assessment-research-report-data.ts +2 -2
- package/src/mount-orpc-router.test.ts +97 -1
- package/src/mount-orpc-router.ts +33 -9
- package/src/register-registry-page-route/region-slots-page-data.ts +2 -2
package/package.json
CHANGED
|
@@ -105,3 +105,32 @@ describe("resolveReportHalves", () => {
|
|
|
105
105
|
).rejects.toThrow(/matches no quiz response/);
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
describe("superseded-pass leak prevention", () => {
|
|
110
|
+
test("the live pass reaches the reader and the superseded pass's wording never does", async () => {
|
|
111
|
+
const SUPERSEDED_MARKER = "SUPERSEDED-PASS-64-PARKED-DOMAIN-CLAIM";
|
|
112
|
+
const superseded = annaResearch({
|
|
113
|
+
id: "research-superseded",
|
|
114
|
+
superseded_at: "2026-09-04T01:00:00Z",
|
|
115
|
+
findings: { intro: `${SUPERSEDED_MARKER}: no website exists.` },
|
|
116
|
+
});
|
|
117
|
+
const live = annaResearch({ id: "research-live", findings: { intro: "launcha.marketing is live and names you." } });
|
|
118
|
+
const client = {
|
|
119
|
+
assessment_research: {
|
|
120
|
+
findFirst: async () => live,
|
|
121
|
+
},
|
|
122
|
+
assessment_responses: {
|
|
123
|
+
findFirst: async () => null,
|
|
124
|
+
findMany: async () => [],
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const halves = await resolveReportHalves({
|
|
128
|
+
input: { response: null, research: superseded, id: superseded.id },
|
|
129
|
+
client,
|
|
130
|
+
params: {},
|
|
131
|
+
});
|
|
132
|
+
const text = JSON.stringify(halves.research);
|
|
133
|
+
expect(text).not.toContain(SUPERSEDED_MARKER);
|
|
134
|
+
expect(text).toContain("launcha.marketing is live and names you");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -128,7 +128,7 @@ describe("research shaping", () => {
|
|
|
128
128
|
});
|
|
129
129
|
expect(data.research.combinedStatement).toContain("17 points higher");
|
|
130
130
|
expect(data.research.combinedBand).toBe("Recognised");
|
|
131
|
-
expect(data.research.reportDate).toBe("2026-09-
|
|
131
|
+
expect(data.research.reportDate).toBe("2026-09-04");
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
test("research-only person: the research score drives the snapshot and the self sections stay absent", () => {
|
|
@@ -229,34 +229,5 @@ describe("the rendered text surface is one-person clean", () => {
|
|
|
229
229
|
expect(strings).not.toContain(marker);
|
|
230
230
|
}
|
|
231
231
|
});
|
|
232
|
-
|
|
233
|
-
// "a different person's research never renders here" is enforced UPSTREAM:
|
|
234
|
-
// the pairing resolver matches halves by normalised LinkedIn identity, and
|
|
235
|
-
// its refusal tests live in fn-routers/assessment-report-pair.test.ts.
|
|
236
232
|
});
|
|
237
233
|
|
|
238
|
-
describe("small derivations", () => {
|
|
239
|
-
test("gapText is signed and plain", () => {
|
|
240
|
-
expect(gapText(54, 71)).toBe("+17");
|
|
241
|
-
expect(gapText(71, 54)).toBe("-17");
|
|
242
|
-
expect(gapText(54, 54)).toBe("0");
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test("prepared-for line reads exactly brand and person", () => {
|
|
246
|
-
expect(buildPreparedFor("Launcha Marketing", "Anna Osherov")).toBe(
|
|
247
|
-
"Launcha Marketing and Anna Osherov",
|
|
248
|
-
);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("combined statement names the direction, size and band", () => {
|
|
252
|
-
const statement = buildCombinedStatement({
|
|
253
|
-
selfScore: 54,
|
|
254
|
-
researchScore: 71,
|
|
255
|
-
bandSteps: 1,
|
|
256
|
-
researchBandName: "Recognised",
|
|
257
|
-
});
|
|
258
|
-
expect(statement).toContain("17 points higher");
|
|
259
|
-
expect(statement).toContain("one band higher");
|
|
260
|
-
expect(statement).toContain("Recognised");
|
|
261
|
-
});
|
|
262
|
-
});
|
|
@@ -229,7 +229,7 @@ export interface ResearchReportData {
|
|
|
229
229
|
methodRawScores: string;
|
|
230
230
|
methodSources: string;
|
|
231
231
|
researchedAt: string;
|
|
232
|
-
/** the report's date: the
|
|
232
|
+
/** the report's date: always the research pass date (when the report was produced) */
|
|
233
233
|
reportDate: string;
|
|
234
234
|
}
|
|
235
235
|
|
|
@@ -373,7 +373,7 @@ export function buildResearchReportData(opts: {
|
|
|
373
373
|
methodRawScores: findings.rawScoresNote ?? "",
|
|
374
374
|
methodSources: findings.sourceSummary ?? "",
|
|
375
375
|
researchedAt: isoDay(research.researched_at),
|
|
376
|
-
reportDate: isoDay(
|
|
376
|
+
reportDate: isoDay(research.researched_at),
|
|
377
377
|
};
|
|
378
378
|
}
|
|
379
379
|
|
|
@@ -13,7 +13,10 @@ import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { mountOrpcRouter } from "./mount-orpc-router.ts";
|
|
16
|
-
|
|
16
|
+
// Captured at module-load time — BEFORE any mock.module() in the test bodies
|
|
17
|
+
// replaces the specifier — so the error-hook test can restore the REAL
|
|
18
|
+
// configured-primitive surface the mount wires the recorder through.
|
|
19
|
+
import * as orpcConfigureReal from "@teamscala/orpc/configure";
|
|
17
20
|
const originalCwd = process.cwd();
|
|
18
21
|
const tempDirs: string[] = [];
|
|
19
22
|
|
|
@@ -85,3 +88,96 @@ describe("mountOrpcRouter generated-artifact seam", () => {
|
|
|
85
88
|
expect(seenKeys).not.toContain("getMcpGatewayUrl");
|
|
86
89
|
});
|
|
87
90
|
});
|
|
91
|
+
|
|
92
|
+
describe("mountOrpcRouter procedure error hook", () => {
|
|
93
|
+
test("a throwing procedure through the mounted route records a durable event with the stack", async () => {
|
|
94
|
+
const service = await mkdtemp(join(tmpdir(), "mount-orpc-router-test-"));
|
|
95
|
+
tempDirs.push(service);
|
|
96
|
+
await mkdir(join(service, "prisma/generated/orpc/routers"), {
|
|
97
|
+
recursive: true,
|
|
98
|
+
});
|
|
99
|
+
await Bun.file(join(service, "prisma/schema.prisma")).write(
|
|
100
|
+
'datasource db { provider = "postgresql" }',
|
|
101
|
+
);
|
|
102
|
+
await Bun.file(
|
|
103
|
+
join(service, "prisma/generated/orpc/routers/index.ts"),
|
|
104
|
+
).write("export const router = {};\n");
|
|
105
|
+
process.chdir(service);
|
|
106
|
+
|
|
107
|
+
const { os } = await import("@orpc/server");
|
|
108
|
+
// The REAL installed orpc surface stays live (configure +
|
|
109
|
+
// procedure-error-report + the RPCHandler the mount builds) so the
|
|
110
|
+
// asserted record proves the wiring, not a mock agreeing with itself.
|
|
111
|
+
// Only the app router source and the event-log write boundary are
|
|
112
|
+
// replaced. The earlier test in this file partially mocked the
|
|
113
|
+
// configure specifier; restore the real exports first. getPrisma is
|
|
114
|
+
// configured because the route reads it per request.
|
|
115
|
+
mock.module("@teamscala/orpc/configure", () => orpcConfigureReal);
|
|
116
|
+
const { configure: orpcConfigure } = await import(
|
|
117
|
+
"@teamscala/orpc/configure"
|
|
118
|
+
);
|
|
119
|
+
orpcConfigure({ getPrisma: () => ({}) as never });
|
|
120
|
+
|
|
121
|
+
const recordedEvents: Array<{ kind: string; payload: unknown }> = [];
|
|
122
|
+
mock.module("@teamscala/orpc/app-router", () => ({
|
|
123
|
+
getAppRouter: async () => ({
|
|
124
|
+
fn: {
|
|
125
|
+
test: {
|
|
126
|
+
boom: os.handler(async () => {
|
|
127
|
+
throw new ReferenceError("mount-boundary boom");
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
}));
|
|
133
|
+
mock.module("@teamscala/event-log/record-event", () => ({
|
|
134
|
+
recordEvent: (input: { kind: string; payload: unknown }) => {
|
|
135
|
+
recordedEvents.push(input);
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
let routeHandler: ((c: unknown) => Promise<Response>) | undefined;
|
|
140
|
+
const app = {
|
|
141
|
+
post: (
|
|
142
|
+
_path: string,
|
|
143
|
+
...handlers: Array<(c: unknown) => Promise<Response>>
|
|
144
|
+
) => {
|
|
145
|
+
routeHandler = handlers.at(-1);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
await mountOrpcRouter(app as never, "scala-mcp");
|
|
149
|
+
expect(routeHandler).toBeTypeOf("function");
|
|
150
|
+
|
|
151
|
+
const response = await routeHandler?.({
|
|
152
|
+
req: {
|
|
153
|
+
raw: new Request("http://localhost/api/rpc/fn/test/boom", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "content-type": "application/json" },
|
|
156
|
+
body: JSON.stringify({}),
|
|
157
|
+
}),
|
|
158
|
+
path: "/api/rpc/fn/test/boom",
|
|
159
|
+
method: "POST",
|
|
160
|
+
},
|
|
161
|
+
notFound: () => new Response(null, { status: 404 }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(response?.status).toBe(500);
|
|
165
|
+
expect(recordedEvents).toHaveLength(1);
|
|
166
|
+
const { kind, payload } = recordedEvents[0];
|
|
167
|
+
expect(kind).toBe("orpc.procedure.failed");
|
|
168
|
+
const record = payload as {
|
|
169
|
+
procedure: string;
|
|
170
|
+
errorName: string;
|
|
171
|
+
message: string;
|
|
172
|
+
errorId: string;
|
|
173
|
+
stackHead: string;
|
|
174
|
+
};
|
|
175
|
+
expect(record.procedure).toBe("fn.test.boom");
|
|
176
|
+
expect(record.errorName).toBe("ReferenceError");
|
|
177
|
+
expect(record.message).toContain("mount-boundary boom");
|
|
178
|
+
expect(record.errorId).toBeTruthy();
|
|
179
|
+
const stackLines = record.stackHead.split("\n");
|
|
180
|
+
expect(stackLines.length).toBeGreaterThanOrEqual(2);
|
|
181
|
+
expect(stackLines[0]).toContain("ReferenceError");
|
|
182
|
+
});
|
|
183
|
+
});
|
package/src/mount-orpc-router.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
14
|
-
import type {
|
|
14
|
+
import type { Db8Client } from "@teamscala/db/db8-registry";
|
|
15
15
|
import type { Hono } from "hono";
|
|
16
16
|
import { bodyLimit } from "hono/body-limit";
|
|
17
17
|
|
|
@@ -94,8 +94,28 @@ export async function mountOrpcRouter(
|
|
|
94
94
|
const router = await getAppRouter();
|
|
95
95
|
if (!router || Object.keys(router).length === 0) return null;
|
|
96
96
|
|
|
97
|
+
// The mount error hook: every procedure error crossing this boundary is
|
|
98
|
+
// shaped by buildErrorReport (errorId, class, stack head) and recorded
|
|
99
|
+
// durably via event-log before ORPC swallows it into a stackless 500.
|
|
100
|
+
// The recorder is wired HERE because orpc is primitives tier and cannot
|
|
101
|
+
// import @teamscala/event-log across a same-tier edge — the boot layer
|
|
102
|
+
// owns that dependency.
|
|
103
|
+
const { procedureErrorReportInterceptor } = await import(
|
|
104
|
+
"@teamscala/orpc/procedure-error-report"
|
|
105
|
+
);
|
|
106
|
+
const { recordEvent } = await import("@teamscala/event-log/record-event");
|
|
107
|
+
const { configure: orpcConfigure } = await import(
|
|
108
|
+
"@teamscala/orpc/configure"
|
|
109
|
+
);
|
|
110
|
+
orpcConfigure({
|
|
111
|
+
procedureErrorRecorder: (record) =>
|
|
112
|
+
recordEvent({ kind: "orpc.procedure.failed", payload: record }),
|
|
113
|
+
});
|
|
114
|
+
|
|
97
115
|
const { RPCHandler } = await import("@orpc/server/fetch");
|
|
98
|
-
const handler = new RPCHandler(router as never, {
|
|
116
|
+
const handler = new RPCHandler(router as never, {
|
|
117
|
+
interceptors: [procedureErrorReportInterceptor()] as never,
|
|
118
|
+
});
|
|
99
119
|
|
|
100
120
|
// The bootloader injects the service Prisma client into ORPC's configured
|
|
101
121
|
// primitive. Reading it through that surface keeps this mount on the same
|
|
@@ -109,7 +129,7 @@ export async function mountOrpcRouter(
|
|
|
109
129
|
// the EXPECTED state for services that have not migrated — db8 stays
|
|
110
130
|
// undefined and v7 procedures are untouched. One boot-time creation,
|
|
111
131
|
// closed over by the request handler (connects lazily on first query).
|
|
112
|
-
let db8:
|
|
132
|
+
let db8: Db8Client | undefined;
|
|
113
133
|
try {
|
|
114
134
|
if (process.env.DATABASE_URL) {
|
|
115
135
|
const { createDbClientV8 } = await import(
|
|
@@ -175,7 +195,12 @@ export async function mountOrpcRouter(
|
|
|
175
195
|
import("@teamscala/db/work-items"),
|
|
176
196
|
import("@teamscala/email-client/client"),
|
|
177
197
|
]);
|
|
178
|
-
configureWorkItems({
|
|
198
|
+
configureWorkItems({
|
|
199
|
+
// The injected contract is the template-send subset; the real
|
|
200
|
+
// client's send agrees at every call site ({to, template, vars}).
|
|
201
|
+
emailSender: createEmailClient() as never,
|
|
202
|
+
logger: log,
|
|
203
|
+
});
|
|
179
204
|
} catch (err) {
|
|
180
205
|
log.warn(
|
|
181
206
|
`[scala-install:${serviceSlug}] work-items email sender not wired: ${err instanceof Error ? err.message : err}`,
|
|
@@ -234,11 +259,10 @@ export async function mountOrpcRouter(
|
|
|
234
259
|
context: { prisma, db8, headers: c.req.raw.headers, resolveUser },
|
|
235
260
|
});
|
|
236
261
|
if (!result?.matched) return c.notFound();
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// an empty orm and every model action returned a silent 500.
|
|
262
|
+
// Response-side signal only (a fleet-wide 500 spike with no
|
|
263
|
+
// traceback — measured 2026-09-04). The real error, its class and
|
|
264
|
+
// its stack are captured by the mount error hook above; this line
|
|
265
|
+
// stays for failures that reach the boundary without a throw.
|
|
242
266
|
if (result.response.status >= 500) {
|
|
243
267
|
log.error(
|
|
244
268
|
`[ORPC] ${c.req.method} ${c.req.path} → ${result.response.status}`,
|
|
@@ -92,7 +92,7 @@ export function regionSlotsPageData(
|
|
|
92
92
|
{
|
|
93
93
|
templateTitle: (meta.title as string) || undefined,
|
|
94
94
|
websiteName: (website.name as string) || undefined,
|
|
95
|
-
organisationName: (brand.companyName as string) || undefined,
|
|
95
|
+
organisationName: (brand.companyFriendlyName as string) || (brand.companyName as string) || undefined,
|
|
96
96
|
entityTitle: entityTitle || undefined,
|
|
97
97
|
},
|
|
98
98
|
{ orgPrefix },
|
|
@@ -164,7 +164,7 @@ export function regionSlotsPageData(
|
|
|
164
164
|
{
|
|
165
165
|
templateTitle: (meta.title as string) || undefined,
|
|
166
166
|
websiteName: (website.name as string) || undefined,
|
|
167
|
-
organisationName: (brand.companyName as string) || undefined,
|
|
167
|
+
organisationName: (brand.companyFriendlyName as string) || (brand.companyName as string) || undefined,
|
|
168
168
|
pageName: contentPageName,
|
|
169
169
|
entityTitle: entityTitle || undefined,
|
|
170
170
|
},
|