@teamscala/service-boot 0.23.524 → 0.23.525

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.
Files changed (81) hide show
  1. package/package.json +1 -9
  2. package/src/agreement-gate/authorized-emails.ts +0 -7
  3. package/src/agreement-gate/placeholder-strip.ts +0 -2
  4. package/src/agreement-gate/settings.ts +0 -15
  5. package/src/agreement-gate.ts +0 -34
  6. package/src/attach-proposal-accept.ts +1 -1
  7. package/src/attach-proposal-magic.ts +1 -1
  8. package/src/auth/sign-out.ts +0 -2
  9. package/src/auth-gate/public-paths.ts +0 -86
  10. package/src/auth-gate.ts +0 -19
  11. package/src/bind-page-handler.ts +0 -76
  12. package/src/build-client-assets/hydrate-bundle.ts +0 -126
  13. package/src/build-client-assets/manifest.ts +0 -18
  14. package/src/build-client-assets/source-candidates.ts +0 -34
  15. package/src/build-client-assets/sourcemaps-upload.ts +0 -42
  16. package/src/build-client-assets/styles-build.ts +0 -50
  17. package/src/capability-slug.ts +0 -10
  18. package/src/compose-page-title.ts +0 -9
  19. package/src/content-gate/access.ts +0 -26
  20. package/src/content-gate/catalog.ts +0 -29
  21. package/src/content-gate/config-types.ts +3 -84
  22. package/src/content-gate/gate.ts +0 -20
  23. package/src/content-page-floor.ts +0 -11
  24. package/src/error-page-logo.ts +2 -12
  25. package/src/expand-roster.ts +0 -9
  26. package/src/gpt-gate.ts +1 -1
  27. package/src/hono-error-handler.ts +0 -4
  28. package/src/install-logging.ts +0 -32
  29. package/src/load-page-manifest.ts +0 -6
  30. package/src/load-service-install-row.ts +41 -4
  31. package/src/module-not-found.ts +0 -11
  32. package/src/mount-orpc-router.ts +1 -88
  33. package/src/mount-server-packages.ts +0 -29
  34. package/src/mutable-app-ref.ts +0 -11
  35. package/src/orpc-contract-rows.json +289 -0
  36. package/src/orpc-openapi-document.ts +0 -6
  37. package/src/orpc-openapi-gate.ts +0 -10
  38. package/src/pg-boss-handler-registry.ts +0 -12
  39. package/src/pg-boss-mount.ts +0 -101
  40. package/src/proposal-gate.ts +1 -41
  41. package/src/quiz-gate.ts +1 -9
  42. package/src/register-page-server-client.ts +0 -53
  43. package/src/register-registry-page-route/ai-chat-prefetch.ts +0 -18
  44. package/src/register-registry-page-route/apply-page-branding.ts +0 -90
  45. package/src/register-registry-page-route/captured-loader-config.ts +0 -1
  46. package/src/register-registry-page-route/graph-cache.ts +0 -17
  47. package/src/register-registry-page-route/meta-title.ts +0 -24
  48. package/src/register-registry-page-route/page-data-shared.ts +0 -6
  49. package/src/register-registry-page-route/prefetch-org-id.ts +0 -17
  50. package/src/register-registry-page-route/proposal-approval-email-token.ts +0 -11
  51. package/src/register-registry-page-route/proposal-brand-stamping.ts +0 -20
  52. package/src/register-registry-page-route/public-shell-page-data.ts +0 -26
  53. package/src/register-registry-page-route/resolve-request-graph.ts +0 -57
  54. package/src/register-registry-page-route/run-content-gates.ts +2 -54
  55. package/src/register-registry-page-route/warn-uncovered-blocks.ts +0 -10
  56. package/src/register-registry-page-route.ts +0 -75
  57. package/src/register-static-assets.ts +0 -45
  58. package/src/resolve-brand-favicon.ts +0 -9
  59. package/src/resolve-service-port.ts +0 -11
  60. package/src/resolve-theme-mode.ts +0 -8
  61. package/src/resolvers/app-header.ts +0 -1
  62. package/src/resolvers/fetch-org-brand.ts +0 -1
  63. package/src/resolvers/portal-route-paths.ts +0 -17
  64. package/src/resource-not-found.ts +0 -24
  65. package/src/select-service-routes.ts +0 -14
  66. package/src/theme-fonts-link.ts +0 -5
  67. package/src/two-phase-full-app.ts +0 -76
  68. package/src/two-phase-minimal-app.ts +1 -105
  69. package/src/types.ts +0 -14
  70. package/src/ui-service.ts +1 -42
  71. package/src/content-gate/gate-entry-marker.test.ts +0 -111
  72. package/src/content-gate/token.segmentation.test.ts +0 -76
  73. package/src/content-gate/token.test.ts +0 -65
  74. package/src/content-gate/token.ts +0 -197
  75. package/src/is-react-element-type.test.ts +0 -45
  76. package/src/is-react-element-type.ts +0 -10
  77. package/src/page-server-orpc/hub-mcp-client.ts +0 -14
  78. package/src/pg-boss-mount.test.ts +0 -101
  79. package/src/resource-chat-inject.page-assistant.test.ts +0 -145
  80. package/src/resource-chat-inject.test.ts +0 -240
  81. package/src/resource-chat-inject.ts +0 -357
@@ -26,22 +26,12 @@ export async function mountOrpcRouter(
26
26
  serviceSlug: string,
27
27
  ): Promise<OrpcMountResult | null> {
28
28
  const log = getAppLogger();
29
- // registerFnRouters() is retired: the row-composed bundle (orpc-bundle from
30
- // api_route rows) is the sole fn source. The handwritten registration is
31
- // fully shadowed by generated.fn (all families have rows) — keeping it
32
- // would block generated-only procedures (auth, cacheAdmin, regions,
33
- // realtime) from serving.
34
29
  const routerPath = join(
35
30
  process.cwd(),
36
31
  "prisma/generated/orpc/routers/index.ts",
37
32
  );
38
33
  const schemaPath = join(process.cwd(), "prisma/schema.prisma");
39
34
 
40
- // ORPC forward mode: when SCALA_ORPC_FORWARD_URL is set and there is no
41
- // local bundle, /api/rpc/* proxies to the central ORPC surface instead of
42
- // mounting a per-service router. This is the collapse's consolidation path —
43
- // services shed their local bundle and the central surface serves all
44
- // row-composed procedures.
45
35
  const forwardUrl = process.env.SCALA_ORPC_FORWARD_URL;
46
36
  if (!await Bun.file(routerPath).exists() && forwardUrl) {
47
37
  const base = forwardUrl.replace(/\/+$/, "");
@@ -52,7 +42,7 @@ export async function mountOrpcRouter(
52
42
  method: c.req.method,
53
43
  headers: c.req.raw.headers,
54
44
  body: c.req.raw.body,
55
- // @ts-ignore — duplex is required for streaming bodies in Bun
45
+ // @ts-ignore duplex is required for streaming bodies in Bun
56
46
  duplex: "half",
57
47
  });
58
48
  return new Response(res.body, {
@@ -67,14 +57,6 @@ export async function mountOrpcRouter(
67
57
  return { routeCount: 0 };
68
58
  }
69
59
 
70
- // No generated ORPC router is the EXPECTED state for daemon-only / boot-hook
71
- // services (session-picker, dep-sync, etc.) that own no DB schema or
72
- // api_routes — they serve no /api/rpc surface. Skip silently: importing a
73
- // nonexistent module would throw into the catch below and surface a
74
- // misleading "ORPC router not mounted" WARN on every boot. The catch is
75
- // reserved for GENUINE mount failures (router present but fails to load).
76
- // A service that SHOULD have a router but doesn't is a codegen gap caught by
77
- // API verification, not a boot warning.
78
60
  if (!await Bun.file(routerPath).exists()) {
79
61
  if (await Bun.file(schemaPath).exists()) {
80
62
  throw new Error(
@@ -91,12 +73,6 @@ export async function mountOrpcRouter(
91
73
  const router = await getAppRouter();
92
74
  if (!router || Object.keys(router).length === 0) return null;
93
75
 
94
- // The mount error hook: every procedure error crossing this boundary is
95
- // shaped by buildErrorReport (errorId, class, stack head) and recorded
96
- // durably via event-log before ORPC swallows it into a stackless 500.
97
- // The recorder is wired HERE because orpc is primitives tier and cannot
98
- // import @teamscala/event-log across a same-tier edge — the boot layer
99
- // owns that dependency.
100
76
  const { procedureErrorReportInterceptor } = await import(
101
77
  "@teamscala/orpc/procedure-error-report"
102
78
  );
@@ -114,24 +90,12 @@ export async function mountOrpcRouter(
114
90
  interceptors: [procedureErrorReportInterceptor()] as never,
115
91
  });
116
92
 
117
- // The bootloader injects the service Prisma client into ORPC's configured
118
- // primitive. Reading it through that surface keeps this mount on the same
119
- // dependency boundary as the generated procedures.
120
93
  const { getPrisma: getConfiguredPrisma } = await import(
121
94
  "@teamscala/orpc/configure"
122
95
  );
123
96
 
124
- // Coexistence (Prisma 8 migration): create the v8 chained client when
125
- // the shared platform contract ships inside @teamscala/db. Absence is
126
- // the EXPECTED state for services that have not migrated — db8 stays
127
- // undefined and v7 procedures are untouched. One boot-time creation,
128
- // closed over by the request handler (connects lazily on first query).
129
97
  let db8: Db8Client | undefined;
130
98
  try {
131
- // Named-need address (wrong-database guard, 2026-09-27): the tenant
132
- // plane by name through @teamscala/db's resolver, never an env key
133
- // read at this call site. Null = the deploy wrote nothing for this
134
- // plane, and db8 stays undefined on the existing absence path.
135
99
  const databaseUrl = resolveDbUrlOrNull("tenant");
136
100
  if (databaseUrl) {
137
101
  const { createDbClientV8 } = await import(
@@ -141,9 +105,6 @@ export async function mountOrpcRouter(
141
105
  connectionString: databaseUrl,
142
106
  serviceName: serviceSlug,
143
107
  })) as never;
144
- // Register for db-core internals (registry loaders, raw fallbacks)
145
- // routing v8-first via getDb8() — the same coexistence shape as
146
- // the RPC context injection above.
147
108
  const { registerDb8 } = await import("@teamscala/db/db8-registry");
148
109
  registerDb8(db8 as never);
149
110
  }
@@ -154,26 +115,9 @@ export async function mountOrpcRouter(
154
115
  db8 = undefined;
155
116
  }
156
117
 
157
- // Bridge the composed router into tool-executor so in-process tool
158
- // dispatch (chat agents' work_items/meetings/agreements/proposals
159
- // tools, etc.) resolves against this service's ORPC router + Prisma
160
- // client. Without this every ORPC tool call throws
161
- // "app router provider not configured". Parallel to dev-mcp-boot's
162
- // configureToolExecutor. Calls getAppRouter() fresh each time (not a
163
- // boot-time capture) so the tool-executor always resolves the CURRENT
164
- // memoized router — the tool-generator listener may regenerate the
165
- // ORPC router and call clearAppRouterCache() after boot, producing a
166
- // new router object that a captured closure would miss.
167
118
  const { configure: configureToolExecutor } = await import(
168
119
  "@teamscala/tool-executor/configure"
169
120
  );
170
- // The caller-identity and secret collaborators arrive here (the boot
171
- // layer) — tool-executor cannot import its tier-2 siblings statically
172
- // (vertical-dependency-only: a same-tier edge). Resolved eagerly at
173
- // mount so the injected factories stay synchronous. The retired
174
- // @teamscala/host-command-bus invoker injection no longer happens: the
175
- // bus is retired, and host_command tools execute through the native
176
- // in-process coding path inside tool-executor.
177
121
  const [{ getCallerContext }, { decryptSecretConfig }] =
178
122
  await Promise.all([
179
123
  import("@teamscala/mcp-multi-session/caller-context"),
@@ -186,19 +130,12 @@ export async function mountOrpcRouter(
186
130
  decryptSecretConfig: (raw) => decryptSecretConfig(raw as never),
187
131
  });
188
132
 
189
- // The fn capability primitives (foundation tier) cannot import the email
190
- // client, so the boot layer injects the real sender — the same shape the
191
- // bootloader already uses for getPrisma. Until configured, fn.workItems
192
- // notification procedures degrade to { notified: 0 } with an error log
193
- // (never throw).
194
133
  try {
195
134
  const [{ configureWorkItems }, { createEmailClient }] = await Promise.all([
196
135
  import("@teamscala/db/work-items"),
197
136
  import("@teamscala/email-client/client"),
198
137
  ]);
199
138
  configureWorkItems({
200
- // The injected contract is the template-send subset; the real
201
- // client's send agrees at every call site ({to, template, vars}).
202
139
  emailSender: createEmailClient() as never,
203
140
  logger: log,
204
141
  });
@@ -226,17 +163,7 @@ export async function mountOrpcRouter(
226
163
  id: session.user.id,
227
164
  name: session.user.name ?? undefined,
228
165
  email: session.user.email,
229
- // The active organisation lives on the SESSION
230
- // (activeOrganizationId, set at sign-in by scala-otp's
231
- // session.create hook), NOT on the user object — getSession's
232
- // ResolvedSession.user carries no organisationId. Reading the
233
- // user field left organisationId always null, so the generated
234
- // router's org-scope gate 403'd every non-super-admin write
235
- // ("Organization membership required") on the public-HTTP path.
236
166
  organisationId: session.session.activeOrganizationId ?? null,
237
- // better-auth's admin plugin issues role "admin"; the platform
238
- // also uses "super-admin"/"super_admin". All three are platform
239
- // super-admins for ORPC org-scope bypass.
240
167
  isSuperAdmin:
241
168
  role === "super_admin" ||
242
169
  role === "super-admin" ||
@@ -260,10 +187,6 @@ export async function mountOrpcRouter(
260
187
  context: { prisma, db8, headers: c.req.raw.headers, resolveUser },
261
188
  });
262
189
  if (!result?.matched) return c.notFound();
263
- // Response-side signal only (a fleet-wide 500 spike with no
264
- // traceback — measured 2026-09-04). The real error, its class and
265
- // its stack are captured by the mount error hook above; this line
266
- // stays for failures that reach the boundary without a throw.
267
190
  if (result.response.status >= 500) {
268
191
  log.error(
269
192
  `[ORPC] ${c.req.method} ${c.req.path} → ${result.response.status}`,
@@ -273,16 +196,6 @@ export async function mountOrpcRouter(
273
196
  },
274
197
  );
275
198
 
276
- // THE OpenAPI CONTRACT, served BESIDE the RPC — a GET on the same prefix,
277
- // generated from the router object mounted below, so the document and the
278
- // handler cannot describe different surfaces. Order is free: the methods
279
- // never overlap (`app.post` matches POST alone).
280
- //
281
- // Gated by openApiContractAuthorised, which refuses a proxied request as
282
- // well as a wrong key (guard auth-helpers-fail-closed), so only an
283
- // internal caller — the MCP gateway — reaches this. Its own facts come
284
- // from openApiContractOptions; both units carry the reasoning, and
285
- // contract rows state their behaviour.
286
199
  app.get("/api/rpc/openapi.json", async (c) => {
287
200
  const header = (name: string) => c.req.header(name) ?? null;
288
201
  if (
@@ -89,9 +89,6 @@ export async function mountServerPackages(
89
89
  lastNotFound = err;
90
90
  continue;
91
91
  }
92
- // Real error (syntax, runtime, transitive resolution failure inside the
93
- // loaded module) — surface it directly. Swallowing here is what hides
94
- // the true cause behind a misleading "cannot find <pkg>" further down.
95
92
  throw new BootFatalError(
96
93
  "mount-server-packages",
97
94
  serviceSlug,
@@ -122,30 +119,6 @@ export async function mountServerPackages(
122
119
  }
123
120
  } catch (err) {
124
121
  if (err instanceof BootFatalError) throw err;
125
- // A PLUGIN THAT FAILS TO MOUNT IS UNAVAILABLE, NOT FATAL. `crash-isolation`
126
- // states the law directly — "a failure in one package does not crash the
127
- // host" — and names the plugin failure domain: "a broken plugin degrades
128
- // to 'that plugin unavailable'". This arm used to wrap ANY mount error in
129
- // a BootFatalError, so one plugin's missing config killed a whole service.
130
- //
131
- // MEASURED 2026-09-07: scala-agents-ui, the operator's fleet monitoring UI,
132
- // booted through db, auth, routes and every other server_package, then died
133
- // at @teamscala/opencode because OPENCODE_SERVER_URL was absent. systemd
134
- // retried five times and gave up, agents.scala.business served 502, and the
135
- // operator lost sight of his fleet — over one optional integration missing
136
- // one variable, on a page that needs no opencode connection to render.
137
- //
138
- // THE ASYMMETRY DECIDES IT. A service that boots without one plugin still
139
- // serves every route that plugin did not own, and the gap is loud in this
140
- // log line and counted in the summary below. A service that refuses to boot
141
- // serves NOTHING — including the surface that would have explained why.
142
- //
143
- // NOT-FOUND STAYS FATAL, deliberately: that arm is above and untouched. A
144
- // package that LOADS and then throws is a broken plugin, which is this
145
- // case. A package that cannot be resolved at all means the declared
146
- // dependency set was never installed — an install-integrity failure, where
147
- // booting a half-installed service would serve wrong results while looking
148
- // healthy.
149
122
  degraded.push(pkg);
150
123
  log.error(
151
124
  `[scala-install:${serviceSlug}] server_package "${pkg}" UNAVAILABLE — mount failed; ` +
@@ -158,8 +131,6 @@ export async function mountServerPackages(
158
131
  `[scala-install:${serviceSlug}] Registered ${mountRegistry.handlers.size} handler(s), ` +
159
132
  `${mountRegistry.wsRoutes.size} ws route(s), ` +
160
133
  `${mountRegistry.bootHooks.size} boot hook(s)` +
161
- // NAMED, NEVER COUNTED. A bare "1 degraded" sends the reader hunting for
162
- // which one; the name is the whole value of the line.
163
134
  (degraded.length > 0
164
135
  ? `, ${degraded.length} server_package(s) UNAVAILABLE: ${degraded.join(", ")}`
165
136
  : ""),
@@ -7,18 +7,11 @@ import type { Hono } from "hono";
7
7
  let currentApp: Hono | null = null;
8
8
  let appLabel = "initial";
9
9
 
10
- /**
11
- * Create a new mutable app reference initialized with the provided app.
12
- * This should be called once at boot time.
13
- */
14
10
  export function initMutableAppRef(initialApp: Hono, label: string): void {
15
11
  currentApp = initialApp;
16
12
  appLabel = label;
17
13
  }
18
14
 
19
- /**
20
- * Get the current app. Throws if the ref hasn't been initialized.
21
- */
22
15
  export function getApp(): Hono {
23
16
  if (!currentApp) {
24
17
  throw new Error("Mutable app reference not initialized");
@@ -26,10 +19,6 @@ export function getApp(): Hono {
26
19
  return currentApp;
27
20
  }
28
21
 
29
- /**
30
- * Atomically swap the app to a new instance. This is called after the
31
- * full app is built in the background. Returns the previous label for logging.
32
- */
33
22
  export function swapApp(newApp: Hono, newLabel: string): string {
34
23
  const oldLabel = appLabel;
35
24
  currentApp = newApp;
@@ -120,6 +120,295 @@
120
120
  "package": "@teamscala/service-boot",
121
121
  "suite_type": "function_call",
122
122
  "slug": "boot:openapi-contract-refuses-when-no-key-is-configured"
123
+ },
124
+ {
125
+ "args": [
126
+ {},
127
+ "quiz_token=good; quiz_token_entered=1",
128
+ 1730000000000,
129
+ {
130
+ "cookieName": "quiz_token",
131
+ "verifyToken": {
132
+ "$fn": {
133
+ "returns": {
134
+ "name": "V",
135
+ "email": "v@example.com"
136
+ }
137
+ }
138
+ },
139
+ "onAuthorized": {
140
+ "$fn": {
141
+ "returns": null
142
+ }
143
+ },
144
+ "getResourceId": {
145
+ "$fn": {
146
+ "returns": "res-1"
147
+ }
148
+ },
149
+ "onUnauthorized": {
150
+ "$fn": {
151
+ "returns": null
152
+ }
153
+ },
154
+ "entryCookieName": "quiz_token_entered"
155
+ }
156
+ ],
157
+ "note": "Converts gate-entry-marker.test.ts case 3, including the measured first-shipped-version failure (the clears read after the run() scope saw nothing and left the marker in the browser, measured live 2026-09-14): recording is asserted from the recorded call itself, which a post-scope read cannot fake.",
158
+ "label": "an opting gate with token AND marker authorizes and RECORDS the marker for clearing — one view, then the gate returns",
159
+ "expect": {
160
+ "email": "v@example.com",
161
+ "applies": true,
162
+ "authorized": true
163
+ },
164
+ "export": "runContentGate",
165
+ "module": "src/content-gate/gate.ts",
166
+ "package": "@teamscala/service-boot",
167
+ "fixtures": {
168
+ "stubs": [
169
+ {
170
+ "export": "recordGateEntryClear",
171
+ "module": "./gate-entry-clears.ts",
172
+ "record": "clears",
173
+ "returns": null
174
+ }
175
+ ]
176
+ },
177
+ "outcomes": {
178
+ "clears": [
179
+ {
180
+ "count": 1
181
+ },
182
+ {
183
+ "equals": [
184
+ {
185
+ "args": [
186
+ "quiz_token_entered"
187
+ ]
188
+ }
189
+ ]
190
+ }
191
+ ]
192
+ },
193
+ "suite_type": "function_call",
194
+ "slug": "gate-entry-marker:marker-plus-token-authorizes-and-records-clear"
195
+ },
196
+ {
197
+ "args": [
198
+ {},
199
+ "quiz_token=bad; quiz_token_entered=1",
200
+ 1730000000000,
201
+ {
202
+ "cookieName": "quiz_token",
203
+ "verifyToken": {
204
+ "$fn": {
205
+ "returns": null
206
+ }
207
+ },
208
+ "onAuthorized": {
209
+ "$fn": {
210
+ "returns": null
211
+ }
212
+ },
213
+ "getResourceId": {
214
+ "$fn": {
215
+ "returns": "res-1"
216
+ }
217
+ },
218
+ "onUnauthorized": {
219
+ "$fn": {
220
+ "returns": null
221
+ }
222
+ },
223
+ "entryCookieName": "quiz_token_entered"
224
+ }
225
+ ],
226
+ "note": "Converts gate-entry-marker.test.ts case 4.",
227
+ "label": "marker present but token invalid stays unauthorized and records no clear",
228
+ "expect": {
229
+ "applies": true,
230
+ "authorized": false
231
+ },
232
+ "export": "runContentGate",
233
+ "module": "src/content-gate/gate.ts",
234
+ "package": "@teamscala/service-boot",
235
+ "fixtures": {
236
+ "stubs": [
237
+ {
238
+ "export": "recordGateEntryClear",
239
+ "module": "./gate-entry-clears.ts",
240
+ "record": "clears",
241
+ "returns": null
242
+ }
243
+ ]
244
+ },
245
+ "outcomes": {
246
+ "clears": [
247
+ {
248
+ "count": 0
249
+ }
250
+ ]
251
+ },
252
+ "suite_type": "function_call",
253
+ "slug": "gate-entry-marker:marker-without-valid-token-records-no-clear"
254
+ },
255
+ {
256
+ "args": [
257
+ {},
258
+ "quiz_token=good",
259
+ 1730000000000,
260
+ {
261
+ "cookieName": "quiz_token",
262
+ "verifyToken": {
263
+ "$fn": {
264
+ "returns": {
265
+ "name": "V",
266
+ "email": "v@example.com"
267
+ }
268
+ }
269
+ },
270
+ "onAuthorized": {
271
+ "$fn": {
272
+ "returns": null
273
+ }
274
+ },
275
+ "getResourceId": {
276
+ "$fn": {
277
+ "returns": "res-1"
278
+ }
279
+ },
280
+ "onUnauthorized": {
281
+ "$fn": {
282
+ "returns": null
283
+ }
284
+ }
285
+ }
286
+ ],
287
+ "note": "Converts the hand-written gate-entry-marker.test.ts case 1: the entry marker is opt-in, so every non-opting service behaves exactly as before. Converted 2026-10-07 by the tests-out conversion.",
288
+ "label": "a gate that does NOT declare an entry marker authorizes on any load and records nothing for clearing",
289
+ "expect": {
290
+ "email": "v@example.com",
291
+ "applies": true,
292
+ "authorized": true
293
+ },
294
+ "export": "runContentGate",
295
+ "module": "src/content-gate/gate.ts",
296
+ "package": "@teamscala/service-boot",
297
+ "fixtures": {
298
+ "stubs": [
299
+ {
300
+ "export": "recordGateEntryClear",
301
+ "module": "./gate-entry-clears.ts",
302
+ "record": "clears",
303
+ "returns": null
304
+ }
305
+ ]
306
+ },
307
+ "outcomes": {
308
+ "clears": [
309
+ {
310
+ "count": 0
311
+ }
312
+ ]
313
+ },
314
+ "suite_type": "function_call",
315
+ "slug": "gate-entry-marker:non-opting-authorizes-any-load-records-nothing"
316
+ },
317
+ {
318
+ "args": [
319
+ {},
320
+ "quiz_token=good",
321
+ 1730000000000,
322
+ {
323
+ "cookieName": "quiz_token",
324
+ "verifyToken": {
325
+ "$fn": {
326
+ "returns": {
327
+ "name": "V",
328
+ "email": "v@example.com"
329
+ }
330
+ }
331
+ },
332
+ "onAuthorized": {
333
+ "$fn": {
334
+ "returns": null
335
+ }
336
+ },
337
+ "getResourceId": {
338
+ "$fn": {
339
+ "returns": "res-1"
340
+ }
341
+ },
342
+ "onUnauthorized": {
343
+ "$fn": {
344
+ "returns": null
345
+ }
346
+ },
347
+ "entryCookieName": "quiz_token_entered"
348
+ }
349
+ ],
350
+ "note": "Converts gate-entry-marker.test.ts case 2: the marker, not the token alone, is what skips the gate — the mechanism measured live 2026-09-14 when the quiz_token cookie alone let a refresh past the identity gate.",
351
+ "label": "an opting gate authorizes only the request carrying the marker — a token WITHOUT it is unauthorized, so a refresh shows the gate",
352
+ "expect": {
353
+ "applies": true,
354
+ "authorized": false
355
+ },
356
+ "export": "runContentGate",
357
+ "module": "src/content-gate/gate.ts",
358
+ "package": "@teamscala/service-boot",
359
+ "fixtures": {
360
+ "stubs": [
361
+ {
362
+ "export": "recordGateEntryClear",
363
+ "module": "./gate-entry-clears.ts",
364
+ "record": "clears",
365
+ "returns": null
366
+ }
367
+ ]
368
+ },
369
+ "outcomes": {
370
+ "clears": [
371
+ {
372
+ "count": 0
373
+ }
374
+ ]
375
+ },
376
+ "suite_type": "function_call",
377
+ "slug": "gate-entry-marker:opting-gate-without-marker-is-unauthorized"
378
+ },
379
+ {
380
+ "args": [],
381
+ "note": "Converts the pairing half of gate-entry-marker.test.ts: gate and access endpoint must declare the SAME marker name, or the endpoint's redirect carries a cookie the gate never reads and the one-view never happens. Converted 2026-10-07.",
382
+ "label": "the quiz access endpoint opts into the SAME entry marker as the gate",
383
+ "expect": {
384
+ "export_shape": {
385
+ "member_equals": {
386
+ "entryCookieName": "quiz_token_entered"
387
+ }
388
+ }
389
+ },
390
+ "export": "quizAccessConfig",
391
+ "module": "src/quiz-gate.ts",
392
+ "package": "@teamscala/service-boot",
393
+ "suite_type": "function_call",
394
+ "slug": "quiz-gate:access-endpoint-shares-the-entry-marker"
395
+ },
396
+ {
397
+ "args": [],
398
+ "note": "Converts the pairing half of gate-entry-marker.test.ts: the quiz SSR gate carries the marker name, so a refresh returns to the identity gate (operator ruling 2026-09-14). Converted 2026-10-07.",
399
+ "label": "the quiz gate opts into the one-view entry marker",
400
+ "expect": {
401
+ "export_shape": {
402
+ "member_equals": {
403
+ "entryCookieName": "quiz_token_entered"
404
+ }
405
+ }
406
+ },
407
+ "export": "quizGateConfig",
408
+ "module": "src/quiz-gate.ts",
409
+ "package": "@teamscala/service-boot",
410
+ "suite_type": "function_call",
411
+ "slug": "quiz-gate:gate-declares-the-one-view-entry-marker"
123
412
  }
124
413
  ]
125
414
  }
@@ -3,12 +3,6 @@
3
3
  * @status handwritten
4
4
  */
5
5
 
6
- /**
7
- * Both fallbacks are read through FALSYNESS, not `??`: a header that is
8
- * present but empty named nothing, and `"" ?? "http"` would splice an
9
- * empty scheme into the URL as `://host` — a document that serves and
10
- * cannot be called from (caught by the row passing empty positions).
11
- */
12
6
  import type { OpenApiDocumentOptions } from "@teamscala/orpc/openapi";
13
7
 
14
8
  const FALLBACK_HOST = "localhost";
@@ -3,23 +3,13 @@
3
3
  * @status handwritten
4
4
  */
5
5
 
6
- /**
7
- * The four request facts the gate reads, all optional: a fact the caller
8
- * did not send is absent, never a value to trust. Truthiness reads absence
9
- * and null identically, so a header is an assertion, not a hint.
10
- */
11
6
  export interface OpenApiContractRequest {
12
- /** The raw `authorization` header, absent when the caller sent none. */
13
7
  authorization?: string | null;
14
- /** The configured `SCALA_DEV_KEY`, absent when the service has none. */
15
8
  key?: string | null;
16
- /** `x-forwarded-proto`. Its presence means the request was proxied. */
17
9
  forwardedProto?: string | null;
18
- /** `x-forwarded-for`. Same meaning, checked so neither alone is load-bearing. */
19
10
  forwardedFor?: string | null;
20
11
  }
21
12
 
22
- /** True only for an unproxied request presenting exactly the configured key. */
23
13
  export function openApiContractAuthorised(request: OpenApiContractRequest): boolean {
24
14
  if (!request.key) return false;
25
15
  if (request.forwardedProto || request.forwardedFor) return false;
@@ -7,21 +7,9 @@ import { createRegistry } from "@teamscala/generic-registry/create-registry";
7
7
 
8
8
  export type PgBossJobHandler = (ctx: {
9
9
  config: ScheduledJobConfig;
10
- /** Per-job data payload from a one-shot `boss.send()` — absent for scheduled jobs. */
11
10
  jobData?: unknown;
12
11
  }) => Promise<unknown> | unknown;
13
12
 
14
13
  export const pgBossHandlerRegistry = createRegistry<PgBossJobHandler>(
15
14
  "pg-boss-handlers",
16
15
  );
17
-
18
- /**
19
- * Register a handler for a `scheduled_job.handler_slug`. Idempotent on re-import
20
- * (createRegistry.register overwrites silently — last registration wins).
21
- */
22
- export function registerScheduledJobHandler(
23
- slug: string,
24
- handler: PgBossJobHandler,
25
- ): void {
26
- pgBossHandlerRegistry.register(slug, handler);
27
- }