feat(svrs)!: decisión 2/3 — la barrera server-only vive en la PUERTA de cada motor, y la hace cumplir el build

Firmada por el autor (b54bb3ad0). La convención `*.server.ts` no existía en
CERO ficheros del repo; el alias `$svrs` resolvía libre hacia cliente y nada
habría detenido el import accidental que arrastra `node:crypto` —o un
secreto— al bundle del navegador. La violación no estaba ocurriendo; la
puerta no existía.

- Las ENTRADAS se renombran (`index.server.ts`, `testing.server.ts`) con
  `git mv` para preservar historia, y el ALIAS absorbe el rename: ningún
  consumidor cambia su import. Es la PUERTA, no cada habitación: el resto del
  motor se queda como está.
- SvelteKit rompe el BUILD si un módulo `*.server.ts` entra en el grafo
  cliente: barrera por construcción, del framework anfitrión, sin maquinaria
  propia. `npm run build` VERDE (2m16s) es la prueba de que el grafo sigue
  limpio tras el rename.
- El censo `server-boundary.test.ts` SE QUEDA como segunda capa (cubre lo que
  SvelteKit no compila: node puro, scripts).
- La convención queda escrita como LEY DEL TIER en la doctrina de svrs.

Y una corrección de INSTRUMENTO que este rename destapó: `docs-check`
modelaba los alias como de UN segmento, así que declaró rota una importación
VÁLIDA (`$svrs/auth/testing`, que ahora es clave exacta del mapa) y puso rojo
el gate y con él el hook pre-push compartido. Ahora resuelve como resuelve
Vite —clave más larga primero—. El doc tenía razón y el instrumento estaba
mal: la clase que este corpus existe para cazar, aterrizando sobre el
cazador.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 1 month ago
parent 35d0005de4
commit 64ead84072

@ -802,14 +802,38 @@ function isFrameworkOwned(file: string): boolean {
return rel.startsWith('docs/') || rel.startsWith('src/uix/');
}
function importResolves(head: string, rest: string): boolean {
const base = join(REPO, ALIASES.get(head)!, rest);
function resolvesOnDisk(base: string): boolean {
if (existsSync(base)) return true; // file or directory
return ['.ts', '.svelte', '.svelte.ts', '.js', `${sep}index.ts`].some((suffix) =>
existsSync(base + suffix)
);
}
/**
* Resolve an import spec the way VITE does: the alias map holds MULTI-SEGMENT
* keys (`$svrs/auth/testing` points at a file, not a directory), so matching
* only the first segment is a model of aliases the repo outgrew. Longest key
* first, exactly like the resolver whose truth this instrument claims to
* mirror.
*
* Measured 2026-08-27: with the server-only barrier the auth entries became
* `index.server.ts` / `testing.server.ts` and gained exact alias keys; the
* head-only lookup then declared a VALID import broken and turned the gate
* (and the shared pre-push hook) red. The doc was right and the instrument
* was wrong — the class this whole corpus exists to catch, landing on the
* catcher.
*/
function specResolves(spec: string): boolean {
let longest: string | undefined;
for (const key of ALIASES.keys()) {
if (spec !== key && !spec.startsWith(`${key}/`)) continue;
if (!longest || key.length > longest.length) longest = key;
}
if (!longest) return false;
const rest = spec === longest ? '' : spec.slice(longest.length + 1);
return resolvesOnDisk(join(REPO, ALIASES.get(longest)!, rest));
}
for (const file of codeCorpus) {
const rawLines = read(file).split('\n');
for (const fence of codeFences(rawLines)) {
@ -846,7 +870,7 @@ for (const file of codeCorpus) {
line,
`\`${spec}\` — \`${head}\` is not a repo alias (vite.config.ts)`
);
} else if (!importResolves(head, rest)) {
} else if (!specResolves(spec)) {
report(
'error',
'I8-imports',

@ -0,0 +1,57 @@
# svrs — the server-authoritative tier
`src/svrs/` holds the engines that own server authority: secrets, credential
stores, `node:crypto`, runtime enforcement. Nothing in this tier is allowed to
reach a browser bundle.
## Law of the tier: the GATE is a `*.server.ts` module
Each engine's **entry** — the file the alias points at — is named
`*.server.ts`. Not each room of the engine: the door.
| Entry | Alias |
| --------------------------------- | -------------------- |
| `src/svrs/auth/index.server.ts` | `$svrs/auth` |
| `src/svrs/auth/testing.server.ts` | `$svrs/auth/testing` |
The alias absorbs the name, so **no consumer import changes**: code still
writes `from '$svrs/auth'`. The alias entries live in `vite.config.ts` (the
source) and are mirrored in `svelte.config.js`; because they are prefix
matches, the longer specifier must be listed first.
### Why the gate and not every room
`*.server.ts` is native to SvelteKit, and its guard is **transitive**. The
plugin (`vite-plugin-sveltekit-guard`, `@sveltejs/kit/src/exports/vite/index.js`)
tests `/.*\.server\..+/` against the basename of any module loaded in a
non-SSR pass, then walks the import graph back to the route entrypoints and
**fails the build** naming the chain. A client module three hops away from the
gate is caught just as surely as one that imports it directly — so sealing the
door seals every room behind it, including the `src/svrs/index.ts` barrel that
re-exports `AuthServer`.
The barrier therefore costs no machinery of our own: the host framework
enforces it at build time, which is the only moment that matters.
Two carve-outs are worth knowing, both by design:
- The guard skips `ssr === true` loads, so server code imports the gate freely.
- The guard is disabled when `process.env.TEST === 'true'`, so Vitest is
unaffected — which is exactly why the second layer below still exists.
## Second layer: the census
`src/svrs/auth/test/server-boundary.test.ts` walks `src/arts`, `src/uix` and
`web` and fails on the first `$svrs/auth` import. It covers the contexts the
SvelteKit guard cannot see: plain Node, scripts, and any consumer built
outside a SvelteKit client pass. Keep both — they fail in different worlds.
## Engines
- [`auth/`](auth/README.md) — authentication authority. **Gated.**
- [`perm/`](perm/README.md) — authorization authority.
- [`cache/`](cache/README.md) — server cache authority.
`perm` and `cache` have not been gated yet; their entries are still plain
`index.ts`. Extending the law to them is the same two-line move: rename the
entry, add the alias.

@ -7,6 +7,12 @@ emiten eventos de seguridad y se exponen handlers HTTP.
No importes esta capa desde componentes Svelte de cliente. Para UI usa
`$auth` o `App.createActiveAuth()`.
La puerta del motor es un módulo server-only nativo de SvelteKit
(`index.server.ts` / `testing.server.ts`): si un módulo de cliente llega hasta
ella, aunque sea a tres saltos, el build falla nombrando la cadena. El alias
absorbe el nombre, así que el import sigue siendo `$svrs/auth`. La ley del
tier, en [`../README.md`](../README.md).
## Crear el Engine
```ts

@ -8,10 +8,13 @@ import { describe, expect, it } from 'vitest';
* The auth engine is server authority: it reaches `node:crypto`, holds the
* CSRF signing key and talks to the credential store. Nothing that ships to a
* browser may import it — UI consumes `$auth` / `App.createActiveAuth()`
* instead. The repo has no `*.server.ts` convention to enforce that
* mechanically (introducing one is a framework decision for the author, not
* this sweep), so the boundary is held here: a walk of the client-side trees
* that fails on the first import.
* instead. The first line of defence is now the framework's: the engine's
* gate is a native `*.server.ts` module (`src/svrs/README.md`), so SvelteKit
* fails the client build on any chain that reaches it. This census is the
* SECOND layer, and it is not redundant — SvelteKit's guard is disabled under
* `process.env.TEST` and absent altogether outside a SvelteKit client pass
* (plain Node, scripts). So the boundary is also held here: a walk of the
* client-side trees that fails on the first import.
*
* `$svrs/auth` appears many times inside the docs site as *prose and code
* samples* — template literals holding snippets. Those are text, not edges,

@ -1,3 +1,3 @@
export * as AuthServer from './auth/index.ts';
export * as AuthServer from './auth/index.server.ts';
export * as CacheServer from './cache/index.ts';
export * as PermServer from './perm/index.ts';

@ -51,6 +51,11 @@ const config = {
'$packs': resolve(__dirname, 'src/packs'),
// ── Server-authoritative engines (src/svrs) ─────────────────────
// The GATE of each engine is a native SvelteKit server-only
// module (`*.server.ts`); the alias points at the file so no
// consumer import changes. Longest specifier first.
'$svrs/auth/testing': resolve(__dirname, 'src/svrs/auth/testing.server.ts'),
'$svrs/auth': resolve(__dirname, 'src/svrs/auth/index.server.ts'),
'$svrs': resolve(__dirname, 'src/svrs'),
// ── UIX layers (src/uix) ────────────────────────────────────────

@ -45,6 +45,11 @@ const aliases = {
'$packs': resolve(__dirname, 'src/packs'),
// ── Server-authoritative engines (src/svrs) ───────────────────────────
// The GATE of each engine is a native SvelteKit server-only module
// (`*.server.ts`), so the alias points at the file and no consumer
// import changes. Longest specifier first: these are prefix matches.
'$svrs/auth/testing': resolve(__dirname, 'src/svrs/auth/testing.server.ts'),
'$svrs/auth': resolve(__dirname, 'src/svrs/auth/index.server.ts'),
'$svrs': resolve(__dirname, 'src/svrs'),
// ── UIX layers (src/uix) ──────────────────────────────────────────────

Loading…
Cancel
Save

Powered by TurnKey Linux.