You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

381 lines
14 KiB

<script lang="ts">
import Callout from '../../_components/Callout.svelte';
import CodeBlock from '../../_components/CodeBlock.svelte';
import AiAgentsBox from '../../_components/AiAgentsBox.svelte';
import ModuleHeader from '../../_components/ModuleHeader.svelte';
import PageNav from '../../_components/PageNav.svelte';
const serverImports = `import {
AuthServer,
CacheServer,
PermServer
} from '$svrs';
const Auth = AuthServer.createEngineAuth(options);
const Perms = PermServer.createEnginePerms(options);
const Cache = CacheServer.createEngineCache(options);`;
const authExample = `import {
createEngineAuth,
createMemoryAuthAdapter,
createMemoryAuthActors,
createMemoryAuthSessPort,
createWebCryptoAuthCrypto,
createTestPasswordHasher
} from '$svrs/auth';
const Auth = createEngineAuth({
security: {
csrf: {
enabled: true,
signingKey,
ttlMs: 30 * 60_000
},
password: {
minLengthWithoutMfa: 15,
minLengthWithMfa: 8
}
},
ports: {
store: createMemoryAuthAdapter(),
actors: createMemoryAuthActors(),
sess: createMemoryAuthSessPort(),
logger,
timer: { nowMs: () => Date.now() },
crypto: createWebCryptoAuthCrypto(),
passwordHasher: createTestPasswordHasher()
}
});`;
const permExample = `import {
createEnginePerms,
createPermHttpHandlers,
definePermSchema,
definePolicies,
allow,
attr
} from '$svrs/perm';
const schema = definePermSchema({
actors: { user: { attributes: { role: 'string' } } },
resources: { invoice: { actions: ['read', 'pay'] } }
});
const Perms = createEnginePerms({
schema,
policies: definePolicies(schema, [
allow('invoice.read').when(attr('actor.role').eq('admin'))
]),
logger
});
const handlers = createPermHttpHandlers(Perms, resolveActorFromRequest);`;
const cachExample = `import {
createEngineCache,
memoryCacheAdapter,
defaultCachePolicies
} from '$svrs/cache';
const Cache = createEngineCache({
namespace: 'api',
adapter: memoryCacheAdapter({ maxEntries: 5_000 }),
policies: defaultCachePolicies(),
scopeResolver: async () => ({
tenantId: event.locals.tenantId,
actorId: event.locals.auth?.actor?.id,
permissionHash: event.locals.permissionsHash
}),
logger
});`;
const svelteKit = `// +hooks.server.ts
import { createSvelteKitAuthHandle } from '$svrs/auth';
export const handle = createSvelteKitAuthHandle(Auth, {
localsKey: 'auth'
});
// +server.ts or +page.server.ts
export const POST = async ({ request }) => {
await Perms.assert({
actor: await resolveActor(request),
action: 'invoice.pay',
resource: await loadInvoice(request),
context: { risk: { mfa: true } }
});
return json(await payInvoice(request));
};`;
const commonMistakes = [
{
name: 'Importing active modules into server engines',
why: 'Svelte runes/browser state leaks into server code and breaks SSR boundaries.',
fix: 'Use $libs for contracts and $svrs for server engines.'
},
{
name: 'Duplicating shared constants in svrs',
why: 'HTTP names, error codes and event names drift between client/server layers.',
fix: 'Move shared language to libs before using it from svrs and arts.'
},
{
name: 'Letting adapters become the engine',
why: 'The core becomes tied to one database/provider and loses portability.',
fix: 'Keep ports in the engine and concrete adapters at the edge.'
},
{
name: 'Sending secrets to active clients',
why: 'SSR serialization can leak tokens, password hashes, CSRF secrets or provider data.',
fix: 'Serialize only public snapshots such as AuthCurrentView and permission snapshots scoped to the actor.'
},
{
name: 'Mixing auth, permission and cache responsibilities',
why: 'Identity proof, access decisions and data freshness become impossible to reason about.',
fix: 'Auth proves identity, perm authorizes, cache manages freshness/scopes.'
}
] as const;
const aiAgentRows = [
{
step: 'Server-only imports',
where: 'src/svrs/*/index.ts',
rule: 'Never import active Svelte wrappers or browser-only modules into server engines.'
},
{
step: 'Shared contracts',
where: 'src/libs/auth, src/libs/perm, src/libs/cache',
rule: 'Put reusable types, constants and pure helpers in libs before duplicating them inside a server module.'
},
{
step: 'Ports and adapters',
where: 'src/svrs/*/adapters and src/svrs/*/integrations',
rule: 'Core engines depend on ports. Concrete databases, providers and framework adapters stay at the edge.'
},
{
step: 'Security boundaries',
where: '$svrs/auth, $svrs/perm, $svrs/cache',
rule: 'Auth proves identity, perm authorizes actions and cache preserves private scopes. Do not merge those responsibilities.'
},
{
step: 'Diagnostics',
where: '$libs/logger.Logger plus module consts.ts',
rule: 'Use the shared Logger contract and constants. Server logs must avoid secrets, tokens and raw credentials.'
},
{
step: 'Tests',
where: 'src/svrs/auth/test, src/svrs/perm/test, src/svrs/cache/test and /test/ecosystem',
rule: 'Any server behavior change needs contract tests and at least one integration scenario.'
}
] as const;
</script>
<svelte:head>
<title>Server Modules ($svrs) — Active</title>
</svelte:head>
<article class="article">
<ModuleHeader
section="Server Layer"
title="Server Modules"
alias="$svrs"
summary="Server-authoritative engines for auth, permissions and cache. This is where security decisions, request-scoped composition and backend adapters live."
factories={['createEngineAuth', 'createEnginePerms', 'createEngineCache']}
dependsOn={['$libs/auth', '$libs/perm', '$libs/cache', '$logger', '$timer', '$http']}
layer="Server engines"
/>
<Callout variant="warn" title="Boundary">
<p>
Anything that proves identity, grants access or protects private data belongs on
the server. <code>arts/*</code> can improve UX, but <code>svrs/*</code> is where the
authoritative decision is made.
</p>
</Callout>
<h2>Overview</h2>
<p>
The framework has three server modules today: <code>$svrs/auth</code>,
<code>$svrs/perm</code> and <code>$svrs/cache</code>. They exist because these artifacts
have a backend half and a frontend half. The shared language lives in
<code>$libs/*</code>, the server authority lives in <code>$svrs/*</code>, and the
reactive browser/client wrappers live under <code>src/arts/*</code>.
</p>
<p>
This is not a duplicate of App. <code>$active-app</code> is a browser/client composition root.
Server code should import explicit engines from <code>$svrs</code> or from each
submodule.
</p>
<h2>Mental model</h2>
<p>
Server modules exist for artifacts that have a real backend authority. The shared
language lives in <code>$libs</code>, the server engine lives in <code>$svrs</code>, and
the reactive browser facade lives in <code>src/arts</code>. That split matters because only
the server has trusted request context, secure cookies, database access and private ports.
</p>
<p>
The usual flow is: server hook/load creates or reads a server engine, resolves request
context, executes auth/permission/cache decisions, then serializes a safe snapshot to the
page. The active client can refresh or mirror that state, but it does not become the
authority.
</p>
<h2>Import shape</h2>
<CodeBlock code={serverImports} lang="ts" />
<table>
<thead>
<tr>
<th>Module</th>
<th>Factory</th>
<th>What it owns</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>$svrs/auth</code></td>
<td><code>createEngineAuth(options)</code></td>
<td>Identity proof, CSRF, password/recovery flows, session binding, device primitives and security events.</td>
</tr>
<tr>
<td><code>$svrs/perm</code></td>
<td><code>createEnginePerms(options)</code></td>
<td>Authorization decisions, policy evaluation, explanations, query plans and HTTP handlers for the active client.</td>
</tr>
<tr>
<td><code>$svrs/cache</code></td>
<td><code>createEngineCache(options)</code></td>
<td>Backend data coherence: query cache, scopes, policies, tags, epochs, explainability and events.</td>
</tr>
</tbody>
</table>
<h2>Auth server</h2>
<p>
<code>$svrs/auth</code> is the server-authoritative authentication module. It uses
ports instead of importing a database, mailer or auth provider directly.
</p>
<CodeBlock code={authExample} lang="ts" title="createEngineAuth()" />
<table>
<thead>
<tr>
<th>Export group</th>
<th>Members</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr><td>Engine</td><td><code>createEngineAuth</code>, <code>EngineAuth</code></td><td>Current view, password flows, CSRF, recovery, devices, OAuth/MFA primitives and event subscriptions.</td></tr>
<tr><td>Handlers</td><td><code>createAuthRouteHandlers</code>, <code>createSvelteKitAuthHandle</code></td><td>HTTP/SvelteKit integration. Default route handlers cover current, CSRF, password, recovery and sign-out.</td></tr>
<tr><td>Adapters</td><td><code>createMemoryAuthAdapter</code>, <code>createDbAuthAdapter</code>, password/crypto/mailer/test adapters</td><td>Persistence and mechanism ports without hard dependency on an ORM/provider.</td></tr>
<tr><td>Integrations</td><td><code>AuthSessPort</code>, <code>AuthCachePort</code>, <code>AuthPermsPort</code>, <code>AuthHttpPort</code></td><td>Ports for session, cache, perm, http, timer and logger.</td></tr>
</tbody>
</table>
<h2>Perms server</h2>
<p>
<code>$svrs/perm</code> is the authorization authority. The browser can ask for a
decision, but protected routes must still call the server engine.
</p>
<CodeBlock code={permExample} lang="ts" title="createEnginePerms()" />
<table>
<thead>
<tr>
<th>Surface</th>
<th>Members</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr><td>Runtime</td><td><code>check</code>, <code>can</code>, <code>assert</code></td><td>Evaluate one decision and enforce fail-closed server behavior.</td></tr>
<tr><td>Inspection</td><td><code>explain</code>, <code>what</code>, <code>who</code></td><td>Debug/audit decisions and build UI action matrices from server truth.</td></tr>
<tr><td>Query support</td><td><code>filter()</code>, <code>createSqlCompiler()</code></td><td>Turn permissions into predicates or query plans where providers support it.</td></tr>
<tr><td>HTTP</td><td><code>createPermHttpHandlers</code></td><td>Remote bridge for <code>ActivePerms</code>; dispatches check, batch, what and explain.</td></tr>
</tbody>
</table>
<h2>Cache server</h2>
<p>
<code>$svrs/cache</code> wraps the pure <code>$libs/cache</code> runtime with disposal,
diagnostics and the server barrel. Use it for server reads, SSR, API handlers and jobs.
</p>
<CodeBlock code={cachExample} lang="ts" title="createEngineCache()" />
<table>
<thead>
<tr>
<th>Surface</th>
<th>Members</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr><td>Read/write</td><td><code>query</code>, <code>get</code>, <code>set</code>, <code>mutate</code></td><td>Read-through cache and mutation-side updates.</td></tr>
<tr><td>Invalidation</td><td><code>invalidate</code>, <code>clear</code></td><td>Invalidate by exact key, prefix or tag; clear where adapter supports it.</td></tr>
<tr><td>Debug</td><td><code>explain</code>, <code>stats</code>, <code>on</code></td><td>Explain a decision, inspect counters and subscribe to cache events.</td></tr>
<tr><td>Adapters</td><td><code>memoryCacheAdapter</code>, <code>storageCacheAdapter</code></td><td>In-memory and storage-backed adapters from the shared cache core.</td></tr>
</tbody>
</table>
<h2>SvelteKit pattern</h2>
<p>
Server modules are created in server-only files and injected into hooks, actions and
endpoints. Client pages receive only serializable snapshots and call active clients for
UX refreshes.
</p>
<CodeBlock code={svelteKit} lang="ts" />
<h2>Layer rules</h2>
<ul>
<li><code>$libs/*</code> defines shared contracts, constants and pure helpers.</li>
<li><code>$svrs/*</code> owns server authority, secrets, ports and backend adapters.</li>
<li><code>src/arts/*</code> owns active/client state and browser ergonomics.</li>
<li><code>$active-app</code> composes client roots; it should not be imported as the server authority.</li>
<li>Do not expose server stores, password hashes, refresh tokens, CSRF secrets or provider tokens to active/client modules.</li>
</ul>
<h2>Common mistakes</h2>
<table>
<thead>
<tr>
<th>Mistake</th>
<th>Why it hurts</th>
<th>Correct pattern</th>
</tr>
</thead>
<tbody>
{#each commonMistakes as mistake (mistake.name)}
<tr>
<td><code>{mistake.name}</code></td>
<td>{mistake.why}</td>
<td>{mistake.fix}</td>
</tr>
{/each}
</tbody>
</table>
<h2>Testing</h2>
<table>
<thead>
<tr>
<th>Target</th>
<th>Purpose</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr><td><code>src/svrs/auth/test</code></td><td>Server auth flows.</td><td>CSRF, password and error guards.</td></tr>
<tr><td><code>src/svrs/perm/test</code></td><td>Perm engine.</td><td>Policy runtime and handler behavior.</td></tr>
<tr><td><code>src/svrs/cache/test</code></td><td>Cache engine.</td><td>Query, invalidation, explain and disposal.</td></tr>
<tr><td><code>/test/ecosystem</code></td><td>Cross-module scenario.</td><td>Client route that exercises active/server interactions where available.</td></tr>
</tbody>
</table>
<AiAgentsBox
intro="Before editing $svrs, verify that the change belongs on the server and that shared contracts are not being duplicated."
rows={aiAgentRows}
/>
<PageNav />
</article>

Powered by TurnKey Linux.