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
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>
|