@teamscala/service-boot 0.23.523 → 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.
- package/package.json +1 -9
- package/src/agreement-gate/agreement-lookup.ts +0 -7
- package/src/agreement-gate/authorized-emails.ts +0 -14
- package/src/agreement-gate/placeholder-strip.ts +0 -10
- package/src/agreement-gate/resolution-caches.ts +0 -5
- package/src/agreement-gate/settings.ts +0 -22
- package/src/agreement-gate/signing-session.ts +0 -8
- package/src/agreement-gate/signing-shell.ts +0 -9
- package/src/agreement-gate.ts +0 -52
- package/src/apply-logger-config.ts +1 -9
- package/src/asset-cache-key.ts +1 -5
- package/src/asset-optimize.ts +1 -7
- package/src/asset-proxy.ts +1 -10
- package/src/asset-social-card.ts +1 -9
- package/src/attach-agreement-magic.ts +0 -14
- package/src/attach-portal-members.ts +0 -17
- package/src/attach-proposal-accept.ts +1 -21
- package/src/attach-proposal-magic.ts +1 -13
- package/src/attach-transfers-upload.ts +0 -21
- package/src/auth/sign-out.ts +1 -44
- package/src/auth-gate/public-paths.ts +1 -96
- package/src/auth-gate.ts +1 -41
- package/src/bind-page-handler.ts +1 -83
- package/src/build-client-assets/build-lock.ts +1 -11
- package/src/build-client-assets/hashed-output.ts +1 -11
- package/src/build-client-assets/hydrate-bundle.ts +1 -137
- package/src/build-client-assets/manifest.ts +1 -30
- package/src/build-client-assets/source-candidates.ts +0 -51
- package/src/build-client-assets/sourcemaps-upload.ts +1 -52
- package/src/build-client-assets/styles-build.ts +1 -71
- package/src/build-client-assets.ts +1 -30
- package/src/capability-slug.ts +1 -45
- package/src/compose-page-title.ts +1 -34
- package/src/content-gate/access.ts +0 -34
- package/src/content-gate/catalog.ts +0 -52
- package/src/content-gate/config-types.ts +3 -90
- package/src/content-gate/gate-entry-clears.ts +0 -18
- package/src/content-gate/gate.ts +0 -27
- package/src/content-gate/parse-cookie.ts +0 -4
- package/src/content-page-floor.ts +0 -28
- package/src/csrf-protection.ts +1 -28
- package/src/error-page-logo.ts +2 -35
- package/src/expand-roster.ts +1 -25
- package/src/gate-ttls.ts +0 -12
- package/src/gpt-gate.ts +1 -19
- package/src/hono-error-handler.ts +0 -19
- package/src/install-harness.ts +1 -12
- package/src/install-logging.ts +1 -47
- package/src/load-page-manifest.ts +1 -29
- package/src/load-service-install-row.ts +42 -10
- package/src/module-not-found.ts +0 -17
- package/src/mount-orpc-router.ts +1 -94
- package/src/mount-server-packages.ts +1 -37
- package/src/mount.ts +1 -4
- package/src/mutable-app-ref.ts +0 -21
- package/src/orpc-contract-rows.json +289 -0
- package/src/orpc-openapi-document.ts +1 -19
- package/src/orpc-openapi-gate.ts +1 -23
- package/src/page-route-shell-types.ts +1 -8
- package/src/page-server-loader-graph.ts +0 -10
- package/src/page-server-orpc/server-client.ts +1 -23
- package/src/pg-boss-handler-registry.ts +1 -24
- package/src/pg-boss-mount.ts +1 -109
- package/src/proposal-authorized-emails.ts +0 -32
- package/src/proposal-gate.ts +1 -52
- package/src/quiz-gate.ts +1 -31
- package/src/read-html-template.ts +1 -49
- package/src/readiness-gate.ts +0 -8
- package/src/register-api-routes.ts +1 -11
- package/src/register-crash-handlers.ts +1 -6
- package/src/register-page-routes.ts +1 -12
- package/src/register-page-server-client.ts +1 -66
- package/src/register-registry-page-route/ai-chat-prefetch.ts +1 -24
- package/src/register-registry-page-route/apply-page-branding.ts +1 -100
- package/src/register-registry-page-route/captured-loader-config.ts +1 -12
- package/src/register-registry-page-route/graph-cache.ts +1 -38
- package/src/register-registry-page-route/live-loader-config.ts +1 -22
- package/src/register-registry-page-route/meta-title.ts +1 -36
- package/src/register-registry-page-route/page-data-shared.ts +1 -18
- package/src/register-registry-page-route/prefetch-org-id.ts +1 -24
- package/src/register-registry-page-route/proposal-approval-email-token.ts +1 -22
- package/src/register-registry-page-route/proposal-brand-stamping.ts +1 -31
- package/src/register-registry-page-route/public-shell-page-data.ts +1 -34
- package/src/register-registry-page-route/region-slots-page-data.ts +1 -8
- package/src/register-registry-page-route/resolve-page-data.ts +1 -10
- package/src/register-registry-page-route/resolve-panel-page.ts +1 -10
- package/src/register-registry-page-route/resolve-request-graph.ts +1 -68
- package/src/register-registry-page-route/run-content-gates.ts +3 -64
- package/src/register-registry-page-route/single-page-data.ts +1 -9
- package/src/register-registry-page-route/stamp-session-context.ts +1 -10
- package/src/register-registry-page-route/warn-uncovered-blocks.ts +1 -18
- package/src/register-registry-page-route.ts +1 -93
- package/src/register-static-assets.ts +1 -53
- package/src/register-ws-routes.ts +1 -21
- package/src/resolve-brand-favicon.ts +0 -22
- package/src/resolve-service-port.ts +1 -61
- package/src/resolve-theme-mode.ts +1 -30
- package/src/resolvers/app-header.ts +1 -20
- package/src/resolvers/fetch-org-brand.ts +1 -11
- package/src/resolvers/portal-route-paths.ts +1 -29
- package/src/resolvers/website-by-host-map.ts +1 -7
- package/src/resource-not-found.ts +0 -48
- package/src/run-boot-hook-list.ts +1 -13
- package/src/run-boot-hooks.ts +1 -19
- package/src/scala-tools-currency-watchdog.ts +1 -7
- package/src/security-headers.ts +1 -7
- package/src/select-service-routes.ts +1 -31
- package/src/server-error-page.ts +0 -18
- package/src/theme-fonts-link.ts +1 -21
- package/src/transfers/presigned-upload.ts +1 -27
- package/src/two-phase-full-app.ts +0 -82
- package/src/two-phase-minimal-app.ts +1 -111
- package/src/two-phase-readiness.ts +0 -6
- package/src/types.ts +0 -17
- package/src/ui-service.ts +2 -48
- package/src/ws-boot-self-test.ts +1 -14
- package/src/content-gate/gate-entry-marker.test.ts +0 -111
- package/src/content-gate/token.segmentation.test.ts +0 -76
- package/src/content-gate/token.test.ts +0 -65
- package/src/content-gate/token.ts +0 -219
- package/src/is-react-element-type.test.ts +0 -45
- package/src/is-react-element-type.ts +0 -20
- package/src/page-server-orpc/hub-mcp-client.ts +0 -20
- package/src/pg-boss-mount.test.ts +0 -101
- package/src/resource-chat-inject.page-assistant.test.ts +0 -145
- package/src/resource-chat-inject.test.ts +0 -240
- package/src/resource-chat-inject.ts +0 -374
|
@@ -1,32 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Tailwind source-candidate scanning for the client CSS build.
|
|
7
|
-
*
|
|
8
|
-
* Split out of build-client-assets.ts (1221 lines, six concerns). Owns ONE
|
|
9
|
-
* concern: turning the resolved `@source` directories into the candidate token
|
|
10
|
-
* set Tailwind's `build()` consumes.
|
|
11
|
-
*
|
|
12
|
-
* Why it exists at all, preserved from the original: Tailwind v4's low-level
|
|
13
|
-
* `compile()` does NOT scan content, and @tailwindcss/oxide's native Scanner is
|
|
14
|
-
* not a dependency of the bare `tailwindcss` lib — only of the integration
|
|
15
|
-
* packages. So the tokens are extracted here. Over-extraction is safe, because
|
|
16
|
-
* `build()` ignores any token that is not a valid utility. Dropping this scan is
|
|
17
|
-
* what shipped an unstyled page to the operator's tablet on 2026-06-13; it is
|
|
18
|
-
* enforced by `css-build-scans-source-candidates`.
|
|
19
|
-
*
|
|
20
|
-
* Byte-for-byte move — imports adjusted, nothing else.
|
|
21
4
|
*/
|
|
22
5
|
|
|
23
6
|
import { resolve } from "node:path";
|
|
24
7
|
import { stat } from "node:fs/promises";
|
|
25
8
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
26
9
|
|
|
27
|
-
// Directory-capable existence probe. Bun.file().exists() answers false for
|
|
28
|
-
// directories, so the @source roots (directories by construction) were all
|
|
29
|
-
// "absent" and the candidate scan returned nothing.
|
|
30
10
|
async function existsStat(path: string): Promise<boolean> {
|
|
31
11
|
try {
|
|
32
12
|
await stat(path);
|
|
@@ -36,16 +16,6 @@ async function existsStat(path: string): Promise<boolean> {
|
|
|
36
16
|
}
|
|
37
17
|
}
|
|
38
18
|
|
|
39
|
-
// Tailwind v4's low-level `compile()` does NOT scan content — `compiler.build( candidates)` only emits utilities for the candidate class names it is GIVEN.
|
|
40
|
-
// `build([])` therefore emits ZERO content-driven utilities (only the static theme/base + @utility/@layer + imported package CSS). The workspace's own
|
|
41
|
-
// blocks mostly use inline styles + hand-written `blk-*` package classes (no-tailwind-in-blocks / layout-properties-inline), so the missing utility
|
|
42
|
-
// layer was invisible — until the Plate document editor (third-party shadcn components using raw `flex`/`p-1`/`bg-background/95`) rendered completely
|
|
43
|
-
// unstyled (operator's tablet, 2026-06-13). The CLI we replaced (`b29e369` "in-process Bun compile") scanned content; the replacement dropped
|
|
44
|
-
// the scan. This restores it: glob every resolved `@source` dir, extract candidate tokens, and feed them to `build()`. @tailwindcss/oxide's native
|
|
45
|
-
// Scanner is NOT a dependency of the bare `tailwindcss` lib (only of the
|
|
46
|
-
// integration packages), so we tokenize ourselves — over-extraction is safe
|
|
47
|
-
// (build() ignores any token that isn't a valid utility). Enforced by
|
|
48
|
-
// `css-build-scans-source-candidates`.
|
|
49
19
|
const SCAN_EXTS = new Set([
|
|
50
20
|
".ts",
|
|
51
21
|
".tsx",
|
|
@@ -57,9 +27,6 @@ const SCAN_EXTS = new Set([
|
|
|
57
27
|
".md",
|
|
58
28
|
".mdx",
|
|
59
29
|
]);
|
|
60
|
-
// Class-name candidate chars: identifiers, variant `:`, opacity `/`, arbitrary
|
|
61
|
-
// values `[]()`, decimals `.`, important `!`, plus `#%,&-` seen in arbitrary
|
|
62
|
-
// values and modifiers. Deliberately broad — invalid tokens are dropped by build().
|
|
63
30
|
const CANDIDATE_RE = /[A-Za-z0-9_:/[\].%#!,&-]+/g;
|
|
64
31
|
|
|
65
32
|
export async function scanSourceCandidates(
|
|
@@ -75,23 +42,7 @@ export async function scanSourceCandidates(
|
|
|
75
42
|
} catch {
|
|
76
43
|
continue;
|
|
77
44
|
}
|
|
78
|
-
// A resolved @source root absent on disk is benign: a published block
|
|
79
|
-
// package ships compiled output, not its src/ tree, so an `@source` into
|
|
80
|
-
// that package resolves to a non-existent dir. Skip silently rather than
|
|
81
|
-
// warn on every UI-service boot (the log-flood class — the ENOENT whose
|
|
82
|
-
// path rendered with a trailing null byte). Real errors below still warn.
|
|
83
|
-
// The probe MUST be a stat, never Bun.file(root).exists(): root is a
|
|
84
|
-
// DIRECTORY and Bun.file() on a directory reports exists() === false
|
|
85
|
-
// (the same defect discover-blocks.ts measured 2026-09-01). That probe
|
|
86
|
-
// skipped EVERY source, collapsed the candidate set to zero, and
|
|
87
|
-
// `build([])` stripped every Tailwind utility from every kind:ui
|
|
88
|
-
// service's stylesheet at once — the operator's "the css is totally
|
|
89
|
-
// fucked" with a stylesheet that loads and 15k package rules intact.
|
|
90
45
|
if (!await existsStat(root)) continue;
|
|
91
|
-
// A root INSIDE node_modules is an explicit `@source` into an installed
|
|
92
|
-
// block package (the app-css transform emits one per package) — scan it
|
|
93
|
-
// fully. A root OUTSIDE node_modules is the service's own tree — scan it
|
|
94
|
-
// but never descend into node_modules (the entire dependency graph).
|
|
95
46
|
const rootInNodeModules = root.includes("/node_modules/");
|
|
96
47
|
try {
|
|
97
48
|
const glob = new Bun.Glob("**/*");
|
|
@@ -114,8 +65,6 @@ export async function scanSourceCandidates(
|
|
|
114
65
|
if (matched) for (const t of matched) candidates.add(t);
|
|
115
66
|
}
|
|
116
67
|
} catch (err) {
|
|
117
|
-
// A bad source dir never aborts the whole CSS build (the other
|
|
118
|
-
// sources still contribute their utilities).
|
|
119
68
|
log.warn(
|
|
120
69
|
`[build-client-assets] candidate scan skipped ${root}: ${
|
|
121
70
|
err instanceof Error ? err.message : String(err)
|
|
@@ -1,44 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Upload source maps to GlitchTip via the official `glitchtip-cli` debug-ID
|
|
7
|
-
* flow, then RETAIN the maps outside the served root so a failed upload is
|
|
8
|
-
* retryable on a later boot.
|
|
9
|
-
*
|
|
10
|
-
* Split out of build-client-assets.ts (962 lines) alongside its build
|
|
11
|
-
* siblings. Byte-for-byte move from the parent — imports adjusted, nothing
|
|
12
|
-
* else.
|
|
13
4
|
*/
|
|
14
5
|
|
|
15
|
-
/**
|
|
16
|
-
* Upload source maps to GlitchTip via the official `glitchtip-cli` debug-ID
|
|
17
|
-
* flow (the documented first-class mechanism — `sourcemaps inject` then
|
|
18
|
-
* `sourcemaps upload --release`; see telemetry.md `client-errors-report-to-glitchtip`
|
|
19
|
-
* + https://glitchtip.com/documentation/cli). Debug IDs injected into the served
|
|
20
|
-
* bundles + uploaded maps are the matching mechanism — robust vs URL-prefix
|
|
21
|
-
* guessing.
|
|
22
|
-
*
|
|
23
|
-
* THE MAPS OUTLIVE THE UPLOAD ATTEMPT: `.map` files are MOVED out of the
|
|
24
|
-
* served root (`dist/client/` — /assets/* would serve source publicly) into
|
|
25
|
-
* the retained cache `dist/sourcemaps/<release>/` BEFORE anything else, and
|
|
26
|
-
* the upload runs from the cache. A failed or skipped upload therefore
|
|
27
|
-
* leaves the maps on disk for the next boot's retry (the fingerprint-skip
|
|
28
|
-
* path in build-client-assets.ts re-runs the upload whenever the manifest
|
|
29
|
-
* does not record `uploaded`) instead of destroying the only copy of a
|
|
30
|
-
* release's maps on an unverified claim (`a-component-may-not-report-a-
|
|
31
|
-
* state-it-has-not-verified`; measured: the pre-2026-09 flow deleted the
|
|
32
|
-
* maps after an upload whose responses were never checked, so a missing
|
|
33
|
-
* token or a 4xx permanently lost a release's maps while the boot log kept
|
|
34
|
-
* saying "uploaded").
|
|
35
|
-
*
|
|
36
|
-
* Every upstream response is verified: the release-create call must return
|
|
37
|
-
* 2xx/208 and each file upload must return 2xx, else the failure is named
|
|
38
|
-
* (status + body head) in the result. Best-effort at the SERVICE level — a
|
|
39
|
-
* failed upload never blocks boot — but never at the CLAIM level: the
|
|
40
|
-
* result says what actually happened and the manifest records it.
|
|
41
|
-
*/
|
|
42
6
|
|
|
43
7
|
import { mkdir, readdir, rename, rm } from "node:fs/promises";
|
|
44
8
|
import { join } from "node:path";
|
|
@@ -50,35 +14,22 @@ export type SourceMapUploadStatus = "uploaded" | "failed" | "skipped";
|
|
|
50
14
|
|
|
51
15
|
export interface SourceMapUploadResult {
|
|
52
16
|
status: SourceMapUploadStatus;
|
|
53
|
-
/** Maps verified accepted by GlitchTip (2xx), never assumed. */
|
|
54
17
|
uploaded: number;
|
|
55
|
-
/** Maps present for this upload pass (moved to the retained cache). */
|
|
56
18
|
total: number;
|
|
57
|
-
/** One named failure per non-accepted upload (status + body head). */
|
|
58
19
|
failures: string[];
|
|
59
20
|
at: string;
|
|
60
21
|
}
|
|
61
22
|
|
|
62
23
|
const UPLOAD_TIMEOUT_MS = 30_000;
|
|
63
24
|
|
|
64
|
-
/** The retained, never-served home of a release's source maps. */
|
|
65
25
|
export function sourceMapsCacheDir(cwd: string, release: string): string {
|
|
66
26
|
return join(cwd, "dist", "sourcemaps", release);
|
|
67
27
|
}
|
|
68
28
|
|
|
69
|
-
/** The release (bare content hash) a manifest's hydratePath names. */
|
|
70
29
|
export function releaseFromHydratePath(hydratePath: string): string | null {
|
|
71
30
|
return /hydrate-([a-z0-9]+)\.js/.exec(hydratePath)?.[1] ?? null;
|
|
72
31
|
}
|
|
73
32
|
|
|
74
|
-
/**
|
|
75
|
-
* Move every `.map` in `outDir` whose base filename is in `currentOutputs`
|
|
76
|
-
* into the retained cache for `release`, then DELETE every other `.map` left
|
|
77
|
-
* in `outDir` (stale maps from an older build whose release cache should
|
|
78
|
-
* already hold a copy — or, on Railway ephemeral disk, was lost; either
|
|
79
|
-
* way the served root must never hold one). Returns the names that were
|
|
80
|
-
* moved into THIS release's cache.
|
|
81
|
-
*/
|
|
82
33
|
async function moveMapsToCache(
|
|
83
34
|
cwd: string,
|
|
84
35
|
outDir: string,
|
|
@@ -121,7 +72,6 @@ function authEnv(): { token: string; url: string } | null {
|
|
|
121
72
|
return { token, url };
|
|
122
73
|
}
|
|
123
74
|
|
|
124
|
-
/** Upload the maps ALREADY RETAINED in the cache for `release`. */
|
|
125
75
|
export async function uploadCachedSourceMaps(
|
|
126
76
|
cwd: string,
|
|
127
77
|
release: string,
|
|
@@ -153,7 +103,6 @@ export async function uploadCachedSourceMaps(
|
|
|
153
103
|
return uploadMapsFromCache(cacheDir, names, release, auth, log, at);
|
|
154
104
|
}
|
|
155
105
|
|
|
156
|
-
/** Upload source maps for the bundle just built into `outDir`. */
|
|
157
106
|
export async function uploadSourceMaps(
|
|
158
107
|
cwd: string,
|
|
159
108
|
outDir: string,
|
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The Tailwind stylesheet unit of the client-asset build: Bun-native
|
|
7
|
-
* stylesheet resolution returning RAW content, stripping of remote
|
|
8
|
-
* (CSP-forbidden) @imports, and the content-hashed styles-<hash>.css
|
|
9
|
-
* build.
|
|
10
|
-
*
|
|
11
|
-
* NESTED @IMPORTS ARE THE SUPPLIER'S JOB: tailwind 4.3.3's compile()
|
|
12
|
-
* re-invokes loadStylesheet for every @import INSIDE loaded content and
|
|
13
|
-
* splices the loaded nodes into the tree (verified in the installed
|
|
14
|
-
* dist source — the dr() recursion, depth-capped at 100), so a present
|
|
15
|
-
* nested leaf is inlined by the supplier and a missing one throws at
|
|
16
|
-
* compile; the suite pins both. The 2026-07-05 dropped-leaf outage was
|
|
17
|
-
* guarded by an in-process flattener here; with the supplier recursion
|
|
18
|
-
* verified the flattener is a second owner of the same fact and is
|
|
19
|
-
* deleted.
|
|
20
|
-
*
|
|
21
|
-
* Split out of build-client-assets.ts (962 lines) alongside its build
|
|
22
|
-
* siblings. Byte-for-byte move from the parent — imports adjusted, nothing
|
|
23
|
-
* else.
|
|
24
4
|
*/
|
|
25
5
|
|
|
26
6
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -42,9 +22,6 @@ export async function bunResolveStylesheet(
|
|
|
42
22
|
return { path: r, base: dirname(r), content: await Bun.file(r).text() };
|
|
43
23
|
}
|
|
44
24
|
if (id.startsWith(".")) {
|
|
45
|
-
// Relative import: resolve against the importing file's directory and
|
|
46
|
-
// REQUIRE existence — a missing leaf is a build-breaking error naming
|
|
47
|
-
// the specifier, never a silently-dropped subtree.
|
|
48
25
|
const p = resolve(base, id);
|
|
49
26
|
if (!(await Bun.file(p).exists())) {
|
|
50
27
|
throw new Error(
|
|
@@ -53,28 +30,11 @@ export async function bunResolveStylesheet(
|
|
|
53
30
|
}
|
|
54
31
|
return { path: p, base: dirname(p), content: await Bun.file(p).text() };
|
|
55
32
|
}
|
|
56
|
-
// A BARE SPECIFIER IS A PACKAGE NAME PLUS AN OPTIONAL SUBPATH, and the two
|
|
57
|
-
// must be separated before anything is looked up. `@teamscala/ui-foundation`
|
|
58
|
-
// is a package; `@teamscala/ui-foundation/theme/styles.css` is that same
|
|
59
|
-
// package plus an explicit file inside it. Treating the whole string as a
|
|
60
|
-
// package name looks for node_modules/@teamscala/ui-foundation/theme/
|
|
61
|
-
// styles.css/package.json, which cannot exist — so EVERY subpath import
|
|
62
|
-
// fails and the styles bundle dies, taking the theme layer with it.
|
|
63
|
-
//
|
|
64
|
-
// A scoped name is the FIRST TWO segments (@scope/name); an unscoped name
|
|
65
|
-
// is the first. Everything after that is a path inside the package and is
|
|
66
|
-
// resolved directly — it names a file, so there is nothing to negotiate and
|
|
67
|
-
// no style entry to consult.
|
|
68
33
|
const segments = id.split("/");
|
|
69
34
|
const nameLength = id.startsWith("@") ? 2 : 1;
|
|
70
35
|
const packageName = segments.slice(0, nameLength).join("/");
|
|
71
36
|
const subpath = segments.slice(nameLength).join("/");
|
|
72
37
|
|
|
73
|
-
// Find node_modules/<packageName>/package.json by walking upward from the
|
|
74
|
-
// importing file's dir then cwd. Bun.resolve is not used: it hands back
|
|
75
|
-
// whatever the package's main is — for a normal JS package a .js entry,
|
|
76
|
-
// not the declared stylesheet — and an `exports` allow-list blocks the
|
|
77
|
-
// package.json subpath on exactly the CSS-only packages this must serve.
|
|
78
38
|
const tried: string[] = [];
|
|
79
39
|
for (const root of [base, cwd]) {
|
|
80
40
|
let dir = root;
|
|
@@ -84,13 +44,6 @@ export async function bunResolveStylesheet(
|
|
|
84
44
|
const pkgDir = dirname(pkgPath);
|
|
85
45
|
const pkg = await Bun.file(pkgPath).json();
|
|
86
46
|
if (subpath) {
|
|
87
|
-
// THE EXPORTS MAP IS THE AUTHORITY ON WHERE A SUBPATH LIVES,
|
|
88
|
-
// and it commonly REMAPS. `@teamscala/ui-foundation` publishes
|
|
89
|
-
// "./theme/styles.css" -> "./src/theme/styles.css", so resolving
|
|
90
|
-
// the subpath literally against the package root looks for a
|
|
91
|
-
// file that has never existed there. Consult exports first and
|
|
92
|
-
// fall back to the literal path only for a package that
|
|
93
|
-
// declares none.
|
|
94
47
|
const exported = pkg.exports?.[`./${subpath}`];
|
|
95
48
|
const mapped: string | undefined =
|
|
96
49
|
typeof exported === "string"
|
|
@@ -107,7 +60,6 @@ export async function bunResolveStylesheet(
|
|
|
107
60
|
}
|
|
108
61
|
return { path: r, base: dirname(r), content: await Bun.file(r).text() };
|
|
109
62
|
}
|
|
110
|
-
// No subpath: the package itself must declare its stylesheet.
|
|
111
63
|
const entry: string | undefined = pkg.exports?.["."]?.style ?? pkg.style;
|
|
112
64
|
if (!entry) {
|
|
113
65
|
throw new Error(
|
|
@@ -128,16 +80,6 @@ export async function bunResolveStylesheet(
|
|
|
128
80
|
);
|
|
129
81
|
}
|
|
130
82
|
|
|
131
|
-
// The app owns its fonts via an explicit, CSP-allowlisted <link> in index.html (Google Fonts — `style-src ... fonts.googleapis.com`, `font-src ...
|
|
132
|
-
// fonts.gstatic.com`). Third-party package CSS MUST NOT inject its own remote `@import url(http…)`: @puckeditor/core/dist/index.css opens with
|
|
133
|
-
// `@import "https://rsms.me/inter/inter.css"`, an arbitrary CDN our CSP does NOT allow. The Tailwind compile inlines that mid-output, where it was either
|
|
134
|
-
// silently dropped (CSS spec: @import must precede all rules) OR — once hoisted —
|
|
135
|
-
// blocked by CSP, leaving a console error on every page; either way the font
|
|
136
|
-
// never loaded and the site fell back to a system font (scala-docs 2026-06-13).
|
|
137
|
-
// So every remote @import is STRIPPED from the compiled bundle: a package can
|
|
138
|
-
// never inject a CDN font/stylesheet, and the only font source is the app-owned
|
|
139
|
-
// index.html link. Fixed once at the single CSS chokepoint; guarded by
|
|
140
|
-
// `css-build-strips-remote-imports`.
|
|
141
83
|
const REMOTE_IMPORT_RE =
|
|
142
84
|
/^[ \t]*@import\s+(?:url\(\s*)?["']https?:\/\/[^"')]+["']\s*\)?[^;]*;[ \t]*\n?/gm;
|
|
143
85
|
|
|
@@ -164,16 +106,6 @@ export async function buildTailwindCss(
|
|
|
164
106
|
await mkdir(outDir, { recursive: true });
|
|
165
107
|
|
|
166
108
|
const cssSource = await Bun.file(entry).text();
|
|
167
|
-
// base MUST be the ENTRY FILE's directory (src/), never the service root.
|
|
168
|
-
// The generated app.css authors its @source dirs relative to itself
|
|
169
|
-
// ("../node_modules/@teamscala/<pkg>/src"); Tailwind 4.3 resolves @source
|
|
170
|
-
// against THIS base, so passing the service root aimed every scan one
|
|
171
|
-
// level too high (workspace/node_modules — empty) and the candidate set
|
|
172
|
-
// collapsed to zero: build([]) strips every Tailwind utility and every
|
|
173
|
-
// kind:ui service renders with naked buttons/controls while the
|
|
174
|
-
// stylesheet still loads with its full package-rule payload (measured
|
|
175
|
-
// 2026-09-05, operator-visible fleet-wide). Package-specifier @imports are
|
|
176
|
-
// unaffected — they resolve through loadStylesheet's Bun.resolve walk.
|
|
177
109
|
const compiler = await compile(cssSource, {
|
|
178
110
|
base: join(cwd, "src"),
|
|
179
111
|
async loadStylesheet(id, base) {
|
|
@@ -186,8 +118,6 @@ export async function buildTailwindCss(
|
|
|
186
118
|
},
|
|
187
119
|
});
|
|
188
120
|
|
|
189
|
-
// Scan every resolved @source for candidate class names — WITHOUT this the
|
|
190
|
-
// build emits no content-driven utilities (the unstyled-editor bug above).
|
|
191
121
|
const candidates = await scanSourceCandidates(compiler.sources, log);
|
|
192
122
|
const cssOutput = stripRemoteImports(compiler.build(candidates));
|
|
193
123
|
log.info(
|
|
@@ -1,35 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Builds the per-service client bundle and stylesheet into `dist/client/`
|
|
7
|
-
* at UI-service boot time. Outputs are content-hashed so deployments
|
|
8
|
-
* naturally cache-bust:
|
|
9
|
-
*
|
|
10
|
-
* src/entry-client.ts ──bun build──> dist/client/hydrate-<hash>.js
|
|
11
|
-
* src/app.css ──tailwindcss──> dist/client/styles-<hash>.css
|
|
12
|
-
* (and) dist/client/asset-manifest.json
|
|
13
|
-
*
|
|
14
|
-
* The HTML template references `{{stylesPath}}` / `{{hydratePath}}` —
|
|
15
|
-
* read-html-template.ts substitutes from the manifest at template-read
|
|
16
|
-
* time. Bytes-level changes → new hash → new URL → browser refetch.
|
|
17
|
-
* Without this, `cache-control: immutable` on a fixed filename means
|
|
18
|
-
* returning visitors keep their stale copy forever.
|
|
19
|
-
*
|
|
20
|
-
* This module is the ORCHESTRATION unit: host-wide lock → parallel
|
|
21
|
-
* hydrate/CSS builds → manifest write (or failure tombstone). The
|
|
22
|
-
* build runs on EVERY boot: the content-fingerprint skip cache and its
|
|
23
|
-
* closure-walking hashers (input-fingerprint.ts, input-hashing.ts) are
|
|
24
|
-
* deleted. A skip is the only path that can serve a stale bundle, and
|
|
25
|
-
* it has done so twice (the mtime skip that baked stale bundles until
|
|
26
|
-
* the fingerprint replaced it, 2026-06-14) — no skip is the one shape
|
|
27
|
-
* that cannot skip wrongly, at the cost of a bounded per-boot build
|
|
28
|
-
* serialised by the host-wide lock below. The individual concerns live
|
|
29
|
-
* as siblings under ./build-client-assets/
|
|
30
|
-
* (source-candidates, manifest, hashed-output, hydrate-bundle,
|
|
31
|
-
* sourcemaps-upload, styles-build, build-lock)
|
|
32
|
-
* — split when the parent held them all at 962 lines.
|
|
33
4
|
*/
|
|
34
5
|
|
|
35
6
|
// Each build is wrapped in its own catch so a JS bundle failure (e.g. transient cross-package export drift) doesn't take down the service
|
package/src/capability-slug.ts
CHANGED
|
@@ -1,59 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Deployable slug -> capability slug.
|
|
7
|
-
*
|
|
8
|
-
* A DEPLOYABLE is named `<capability>-<language>-<tier>`
|
|
9
|
-
* (`deployables-carry-language-and-tier`, as amended by the operator ruling of
|
|
10
|
-
* 2026-09-08: "so actually the apps are ts, not bun" — the language segment
|
|
11
|
-
* spells the LANGUAGE, so Bun-launched services are `ts`): `scala-projects-ts-dev`
|
|
12
|
-
* and `scala-projects-ts-prod` are two DEPLOYMENTS of one APPLICATION. The
|
|
13
|
-
* ecosystem generator derives that name per lane, so one service arrives here
|
|
14
|
-
* under a DIFFERENT identity in each lane.
|
|
15
|
-
*
|
|
16
|
-
* A `page_route` row describes the APPLICATION url space, which is the same
|
|
17
|
-
* in both lanes, so it carries the CAPABILITY name and there is exactly one
|
|
18
|
-
* row per route rather than one per lane. Joining the two by raw string
|
|
19
|
-
* equality therefore matches at most one lane, and does so SILENTLY:
|
|
20
|
-
* measured 2026-09-11, 8 of 10 dev UI services registered ZERO page routes
|
|
21
|
-
* and served 404 on every path -- including `/sign-in` -- while booting
|
|
22
|
-
* clean and reporting healthy, because the dev lane ran as
|
|
23
|
-
* `scala-projects-bun-dev` while every page_route row said `scala-projects`.
|
|
24
|
-
*
|
|
25
|
-
* Both sides normalize through this one function, so the join is
|
|
26
|
-
* tier-agnostic by construction and a row may name either form.
|
|
27
|
-
*
|
|
28
|
-
* The suffix grammar is a CLOSED SET defined by doctrine and emitted by the
|
|
29
|
-
* generator, so this is a parse of a documented contract rather than a
|
|
30
|
-
* pattern heuristic. A slug that does not end in one is already a capability
|
|
31
|
-
* name and is returned unchanged.
|
|
32
|
-
*
|
|
33
|
-
* `bun` REMAINS IN THE SET AS RETIRED SPELLING TOLERANCE, mirroring the
|
|
34
|
-
* generator side (`ecosystem/deployable_identity.rs capability_of` strips
|
|
35
|
-
* `-ts`, `-bun` and `-rust`): the fleet was renamed under `-bun` and rows or
|
|
36
|
-
* supervised names still carrying it must converge to the same capability
|
|
37
|
-
* rather than fail the join during the migration window.
|
|
38
4
|
*/
|
|
39
5
|
|
|
40
|
-
/** Language segment of a deployable name. `bun` is the retired spelling. */
|
|
41
6
|
const LANGUAGES = ["ts", "bun", "rust"] as const;
|
|
42
7
|
|
|
43
|
-
/** Tier segment of a deployable name. */
|
|
44
8
|
const TIERS = ["dev", "prod"] as const;
|
|
45
9
|
|
|
46
10
|
type Language = (typeof LANGUAGES)[number];
|
|
47
11
|
type Tier = (typeof TIERS)[number];
|
|
48
12
|
|
|
49
|
-
/**
|
|
50
|
-
* The capability half of a deployable slug.
|
|
51
|
-
*
|
|
52
|
-
* `scala-projects-ts-dev` -> `scala-projects`
|
|
53
|
-
* `scala-projects-ts-prod` -> `scala-projects`
|
|
54
|
-
* `scala-projects-bun-dev` -> `scala-projects` (retired spelling)
|
|
55
|
-
* `scala-projects` -> `scala-projects` (already a capability name)
|
|
56
|
-
*/
|
|
57
13
|
export function toCapabilitySlug(slug: string): string {
|
|
58
14
|
const parts = slug.split("-");
|
|
59
15
|
if (parts.length < 3) return slug;
|
|
@@ -1,47 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The single canonical page-title composer. Every SSR page handler routes
|
|
7
|
-
* through this (portal/region-shell, public shell, single-page resource). A
|
|
8
|
-
* page title is DERIVED from data that already exists — the website name +
|
|
9
|
-
* the page/entity name — with `builda_pages.meta_title` as an explicit
|
|
10
|
-
* per-page OVERRIDE only, never the primary source.
|
|
11
|
-
*
|
|
12
|
-
* Mirrors the brand-is-org-owned-full-scheme precedent: a value that should
|
|
13
|
-
* be DERIVED was instead hand-set per row, and when unset it silently fell
|
|
14
|
-
* back to something wrong. For themes the wrong fallback was the generic
|
|
15
|
-
* light palette; for titles it was the bare org name — the `default`
|
|
16
|
-
* page_meta_template `titleTemplate: "{org.name}"` produced "GBG Indo" with
|
|
17
|
-
* no site or page context, and per-row `meta_title` carried stale product
|
|
18
|
-
* literals like "Dashboard | scala Hub". The fix is identical in shape:
|
|
19
|
-
* derive by default, override explicitly.
|
|
20
|
-
*
|
|
21
|
-
* Canonical format: `<website.name>: <subject>` (subject = entity title or
|
|
22
|
-
* page name), site-first so a browser tab reads the surface then the page.
|
|
23
|
-
* A resource surface (forms/quiz/gpt) may supply a curated `templateTitle`
|
|
24
|
-
* (e.g. "{agent.name} — {org.name}"); it wins over the default derivation
|
|
25
|
-
* but NOT over an explicit per-page override. The composer NEVER returns a
|
|
26
|
-
* bare org name — that is the regression this closes, and the
|
|
27
|
-
* `page-title-not-bare-org` guard backstops it at the template layer.
|
|
28
4
|
*/
|
|
29
5
|
|
|
30
6
|
export interface TitleContext {
|
|
31
|
-
/** Per-page override (builda_pages.meta_title). Wins outright when set. */
|
|
32
7
|
pageMetaTitle?: string | null;
|
|
33
|
-
/** Resolved resource-template string (forms/quiz/gpt). Curated per-surface shape. */
|
|
34
8
|
templateTitle?: string | null;
|
|
35
|
-
/** builda_websites.name — the surface, the legacy site context (orgPrefix off). */
|
|
36
9
|
websiteName?: string | null;
|
|
37
|
-
/** The OWNING ORG's display name (brand.companyName, resolved from the
|
|
38
|
-
* org the page's brand_org_path names). The title prefix when orgPrefix is
|
|
39
|
-
* on — operator ruling 2026-08-17: a client-facing document's tab carries
|
|
40
|
-
* the org's name, never the internal website label ("Public Pages"). */
|
|
41
10
|
organisationName?: string | null;
|
|
42
|
-
/** builda_pages.name — the page (e.g. "Dashboard"). */
|
|
43
11
|
pageName?: string | null;
|
|
44
|
-
/** Resource entity title (proposal/form/quiz/agent name) — preferred subject. */
|
|
45
12
|
entityTitle?: string | null;
|
|
46
13
|
}
|
|
47
14
|
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The content-gate ACCESS endpoint mounter. One generic POST handler for every
|
|
7
|
-
* gate: a plain <form> posts {<recordIdField>, email}; on a match against the
|
|
8
|
-
* resource's authorized set it sets the signed access cookie and redirects back,
|
|
9
|
-
* so the page loader's gate then releases the content. No bespoke endpoint per
|
|
10
|
-
* type — each gate is one AccessConfig. Mounted BEFORE the auth gate so it is
|
|
11
|
-
* publicly reachable.
|
|
12
4
|
*/
|
|
13
5
|
|
|
14
6
|
import type { Hono } from "hono";
|
|
@@ -38,36 +30,17 @@ export async function attachContentGateAccess(
|
|
|
38
30
|
let email = "";
|
|
39
31
|
let name = "";
|
|
40
32
|
let phone = "";
|
|
41
|
-
// Optional prospect segmentation a capture gate's composed form may post
|
|
42
|
-
// (the quiz identity screen: company, LinkedIn, website, team size). Every
|
|
43
|
-
// field is optional and every reader treats absence as "" — a gate that
|
|
44
|
-
// never collects them (the GPT) is unaffected, and a form that collects
|
|
45
|
-
// only some carries the rest as absent. Field names follow the response
|
|
46
|
-
// columns the segmentation lands in (linkedin_url / team_size, snake case)
|
|
47
|
-
// because the composed form posts exactly what its FormField rows name.
|
|
48
33
|
let company = "";
|
|
49
34
|
let linkedinUrl = "";
|
|
50
35
|
let website = "";
|
|
51
36
|
let teamSize = "";
|
|
52
37
|
let bodyFailed = false;
|
|
53
|
-
// The resource id may ride on the URL instead of the body. A gate whose
|
|
54
|
-
// form is COMPOSED FROM BLOCKS has no place to put a hidden field — the
|
|
55
|
-
// generic submit action collects the visible inputs and adds only the
|
|
56
|
-
// static fields its row declares, and a row is shared by every page so it
|
|
57
|
-
// cannot carry a per-page id. The gate therefore publishes its own access
|
|
58
|
-
// URL with the id already on it (see quiz-gate onUnauthorized), which
|
|
59
|
-
// keeps the id single-sourced from the resolved graph. A body field still
|
|
60
|
-
// wins where one is posted (proposals, agreements, the GPT).
|
|
61
38
|
const queryResourceId = String(c.req.query(config.recordIdField) ?? "").trim();
|
|
62
39
|
try {
|
|
63
40
|
const form = await c.req.parseBody();
|
|
64
41
|
resourceId = String(form[config.recordIdField] ?? "").trim() || queryResourceId;
|
|
65
42
|
email = String(form.email ?? "").trim().toLowerCase();
|
|
66
|
-
// Optional captured display name (open-capture gates carry it in the
|
|
67
|
-
// token; allow-list gates ignore it via signToken).
|
|
68
43
|
name = String(form.name ?? "").trim();
|
|
69
|
-
// Captured alongside the email by an open-capture gate's composed
|
|
70
|
-
// form. It rides in the signed token rather than any client store.
|
|
71
44
|
phone = String(form.phone ?? "").trim();
|
|
72
45
|
company = String(form.company ?? "").trim();
|
|
73
46
|
linkedinUrl = String(form.linkedin_url ?? "").trim();
|
|
@@ -82,9 +55,6 @@ export async function attachContentGateAccess(
|
|
|
82
55
|
|
|
83
56
|
let matched = false;
|
|
84
57
|
if (config.openCapture) {
|
|
85
|
-
// Open lead-capture gate (the public GPT): any non-empty email is a
|
|
86
|
-
// valid visitor — there is no allow-list to authorize against. We
|
|
87
|
-
// already required a non-empty email above, so this is a match.
|
|
88
58
|
matched = true;
|
|
89
59
|
} else {
|
|
90
60
|
try {
|
|
@@ -120,10 +90,6 @@ export async function attachContentGateAccess(
|
|
|
120
90
|
`${config.cookieName}=${encodeURIComponent(token)}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`,
|
|
121
91
|
);
|
|
122
92
|
if (config.entryCookieName) {
|
|
123
|
-
// The ENTRY MARKER: it authorizes exactly the redirect this sets it
|
|
124
|
-
// on — the gate requires it and the render it consumed clears it.
|
|
125
|
-
// Max-Age matches the token only as a safety net; consumption is
|
|
126
|
-
// what ends its life, never the clock.
|
|
127
93
|
c.header(
|
|
128
94
|
"Set-Cookie",
|
|
129
95
|
`${config.entryCookieName}=1; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`,
|
|
@@ -1,58 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* THE CATALOG OF GATED CONTENT TYPES — the one place a gate is registered.
|
|
7
|
-
*
|
|
8
|
-
* The gate ENGINE (gate.ts) and the access MOUNTER (access.ts) were already
|
|
9
|
-
* generic and config-driven, but the SEQUENCE was not: `register-registry-page-
|
|
10
|
-
* route.ts` named proposalGateConfig + agreementGateConfig + gptGateConfig in a
|
|
11
|
-
* hand-written array, and `two-phase-boot.ts` named the same three AccessConfigs
|
|
12
|
-
* in a second hand-written array. Two lists, one fact, and adding a fourth gated
|
|
13
|
-
* resource meant editing both — the same drift as the assistant, one layer down
|
|
14
|
-
* (`information-exists-in-one-location`).
|
|
15
|
-
*
|
|
16
|
-
* Now both call sites iterate THIS. A gate is registered once, as a pair, and
|
|
17
|
-
* the SSR gate and its public access endpoint cannot disagree about which gates
|
|
18
|
-
* exist — the shape doctrine asks for when behaviour genuinely is code:
|
|
19
|
-
* `business-logic-is-rows-plus-executors`, "extend the executor catalog".
|
|
20
|
-
*
|
|
21
|
-
* WHY A CATALOG AND NOT A REGISTRY ROW. A gate is not data — `verifyToken`,
|
|
22
|
-
* `resolveAuthorizedEmails` and the withholding rules are executable behaviour,
|
|
23
|
-
* which the rule places in a typed executor, never in a row. What belongs in a
|
|
24
|
-
* row is the SELECTION: which gate governs a given page. That is a `page_route`
|
|
25
|
-
* field, and a row can only select from a catalog, so this file is the thing it
|
|
26
|
-
* will select from — not a step on the way to it.
|
|
27
4
|
*/
|
|
28
5
|
|
|
29
|
-
/**
|
|
30
|
-
* Registered gates, in evaluation order.
|
|
31
|
-
*
|
|
32
|
-
* ORDER IS LOAD-BEARING AND SO IT IS DECLARED, NOT INCIDENTAL. Proposals run
|
|
33
|
-
* before agreements: an authorized proposal viewer has their gate block removed
|
|
34
|
-
* from the resolved graph, after which the agreement gate finds no resource and
|
|
35
|
-
* no-ops. Reversing these two changes behaviour on a page carrying both.
|
|
36
|
-
*
|
|
37
|
-
* Each gate still self-selects — `getResourceId` returns "" when the page is not
|
|
38
|
-
* its kind — so listing a gate here never makes it apply to a page it does not
|
|
39
|
-
* govern.
|
|
40
|
-
*/
|
|
41
6
|
|
|
42
|
-
/**
|
|
43
|
-
* Does this gate WITHHOLD content, or only identify the visitor?
|
|
44
|
-
*
|
|
45
|
-
* DERIVED FROM THE ACCESS CONFIG, NEVER DECLARED TWICE. `openCapture` already
|
|
46
|
-
* states that any email is a valid visitor, which is precisely the case where
|
|
47
|
-
* nothing is being withheld — the GPT page body is public and only its identity
|
|
48
|
-
* is gated (see gptGateConfig.onUnauthorized, a documented no-op). A separate
|
|
49
|
-
* `withholdsContent` flag would be a second copy of that fact, free to drift
|
|
50
|
-
* from the one the access endpoint actually enforces (`offered-is-derived`).
|
|
51
|
-
*
|
|
52
|
-
* The distinction matters to the assistant: a seed-only gate must not count as
|
|
53
|
-
* "governing" this page, or an anonymous visitor to a public GPT page would be
|
|
54
|
-
* read as an unauthorized viewer and lose an assistant they are entitled to.
|
|
55
|
-
*/
|
|
56
7
|
|
|
57
8
|
import type { AccessConfig, ContentGateConfig } from "./config-types.ts";
|
|
58
9
|
import { agreementAccessConfig, agreementGateConfig } from "../agreement-gate.ts";
|
|
@@ -60,9 +11,7 @@ import { gptAccessConfig, gptGateConfig } from "../gpt-gate.ts";
|
|
|
60
11
|
import { proposalAccessConfig, proposalGateConfig } from "../proposal-gate.ts";
|
|
61
12
|
import { quizAccessConfig, quizGateConfig } from "../quiz-gate.ts";
|
|
62
13
|
|
|
63
|
-
/** One gated content type: its SSR gate and its public access endpoint. */
|
|
64
14
|
export interface ContentGateEntry {
|
|
65
|
-
/** Stable identity for this gate — what a `page_route` will name to select it. */
|
|
66
15
|
slug: string;
|
|
67
16
|
gate: ContentGateConfig;
|
|
68
17
|
access: AccessConfig;
|
|
@@ -79,7 +28,6 @@ export function withholdsContent(entry: ContentGateEntry): boolean {
|
|
|
79
28
|
return entry.access.openCapture !== true;
|
|
80
29
|
}
|
|
81
30
|
|
|
82
|
-
/** The gates that can withhold content, in order — the assistant's authority. */
|
|
83
31
|
export function contentWithholdingGates(): ContentGateEntry[] {
|
|
84
32
|
return CONTENT_GATES.filter(withholdsContent);
|
|
85
33
|
}
|