Skip to content

@kernl/guard ​

Ingress flood control, visibility gating, focus-aware firewall, CSP profile and nested-frame insertion lock for the kernl shell. Pure: no DOM, no timers. Every factory takes an optional now clock and defaults to Date.now.

FieldValue
Package@kernl/guard
Version0.1.0
Entry./dist/index.js, types ./dist/index.d.ts
Dependencies@kehto/firewall ^0.6.3
Peer dependencies@napplet/core >=0.37.0 <0.38.0
Side effectsnone

Install ​

@kernl/guard is not yet published to npm, so pnpm add @kernl/guard fails. During the alpha it is built from the kernl source repository, which is private, and consumed as a workspace dependency. The package index has the steps.

json
{ "dependencies": { "@kernl/guard": "workspace:^" } }

Purpose ​

The guard is the set of protections between an applet and the user. kehto's @kehto/firewall supplies a pure evaluate() engine; this package gives its opaque operation classes meaning, adds the layers kehto's ShellAdapter cannot yet host (ingress flood control, the visibility gate, focus supplied by the shell's window manager), fixes the CSP, adds an insertion lock on top of kehto's nested-context lock (D-014), and describes every mitigation for the UI. The concepts and the numbers are in Guard.

Exports ​

Profiles and configuration ​

ExportSignatureDescription
GUARD_PROFILESreadonly GuardProfile[]['strict', 'balanced', 'relaxed'], tightest first. strict is the shell default (D-003).
createGuardConfig(profile: GuardProfile) => GuardConfigThe profile's GuardLimits plus the derived kehto FirewallConfig: defaultRate from the generic class, burstGuard from ingressBurst, unfocusedMultiplier, and three block content matchers (size:resource:bytes, size:relay:subscribe, size:outbox:subscribe) at cap + 1 bytes. No per-applet rules.
applyNappletRules(config: GuardConfig, napplet: string) => GuardConfigReturns a new config whose firewall carries every class rate as a per-applet rateLimits entry plus a globalRate equal to the generic rate, all with action block. Existing policy and matchers are kept; rates for the applet are overwritten. The input is not modified.
GuardProfile'strict' | 'balanced' | 'relaxed'Profiles change limits only; every mitigation stays on in every profile.
GuardLimitsinterfaceingressBurst, ingressQueue, quarantineThreshold, hiddenResourceAllowance, deferQueue, maxResourceEnvelopeBytes, maxSubscribeEnvelopeBytes, rates (per OpClass: { capacity, windowMs }), unfocusedMultiplier.
GuardConfiginterface{ profile, limits, firewall }.
AdmitOptionsinterface{ hold?: boolean } for IngressGuard.admit; hold: false reports a burst as defer without queueing the envelope.

Classification ​

ExportSignatureDescription
classifyEnvelope(envelope: GuardEnvelope) => OpClassMaps an envelope type (and kind for publishes) to one of the eleven classes. relay.publish with kind 5 is relay:publish:delete; unknown types are generic. The full map is in Guard.
estimateEnvelopeSize(value: unknown) => numberUTF-8 byte length of JSON.stringify(value); 0 when the value cannot be serialised. Use it to fill GuardEnvelope.size.
OpClassunionrelay:subscribe, relay:publish, relay:publish:delete, outbox:subscribe, outbox:publish, resource:bytes, storage:write, dm:send, intent:invoke, control, generic.
GuardEnvelopeinterface{ type: string; kind?: number; size?: number }: the only fields the guard reads.
WindowVisibilityinterface{ visible: boolean; focused: boolean; msSinceFocusGain?: number } as observed by the shell, never self-reported.

Ingress guard ​

ExportSignatureDescription
createIngressGuard(options: IngressGuardOptions) => IngressGuardPer-window token bucket of ingressBurst per second, a queue of ingressQueue, quarantine at quarantineThreshold arrivals in a rolling 10 s window or past hiddenResourceAllowance resource requests while hidden, and envelope size caps.
IngressGuardOptionsinterface{ config: GuardConfig; now?: () => number; onEvent?: (event: GuardEvent) => void; drainBatch?: number }. drainBatch defaults to 4.
IngressGuardinterfaceadmit(windowId, napplet, envelope, visibility, options?: AdmitOptions): GuardDisposition ({ hold: false } reports a defer without queueing the envelope), drain(windowId): readonly GuardEnvelope[], isQuarantined(windowId): boolean, release(windowId): void (lifts quarantine, restores a fresh budget), forget(windowId): void (drops all state).
QUARANTINE_WINDOW_MS10_000The rolling window over which arrivals are counted against quarantineThreshold.
GuardDisposition'pass' | 'defer' | 'reject' | 'quarantine'What admit returns. defer here means the burst budget is spent and the envelope was queued, not that the window is hidden.
GuardEventinterface{ disposition: GuardDisposition | 'flag'; windowId; napplet; opClass; reason; at }. Emitted for every disposition other than pass, and for a pass the firewall flagged. reason is a stable prefixed string; the list is in Guard.

Visibility gate ​

ExportSignatureDescription
createVisibilityGate(options: VisibilityGateOptions) => VisibilityGateHolds a hidden window's traffic (everything except control) in a queue of deferQueue and releases it, oldest first, when the window returns.
VisibilityGateOptionsinterface{ config: GuardConfig; onEvent?; now?; nappletFor?: (windowId: string) => string }. Without nappletFor, overflow events name the applet unknown.
VisibilityGateinterfacesetVisibility(windowId, visibility), getVisibility(windowId) (unreported windows are { visible: true, focused: false }), shouldDefer(windowId, opClass): boolean, defer(windowId, envelope): boolean (false and a reject event with reason defer:queue-full when full), drain(windowId), forget(windowId).

Focus firewall ​

ExportSignatureDescription
createFocusFirewall(options: FocusFirewallOptions) => FocusFirewallkehto's evaluate() fed with the shell's focused flag. Registers the applet's class rates through applyNappletRules on first observation. reject emits reject; pass with action flag emits flag; prompt emits reject with reason prompt:<ruleId>.
FocusFirewallOptionsinterface{ config: GuardConfig; now?: () => number; onEvent?: (event: GuardEvent) => void }.
FocusFirewallinterfaceobserve(windowId, napplet, envelope, visibility): EvaluateResult (kehto's result, with newState already applied), forget(windowId): void. kehto keys budgets by applet, so forget also resets the budget of another window running the same applet.

CSP ​

ExportSignatureDescription
createAppletCsp(profile: GuardProfile) => AppletCspkehto's zero-network baseline: 14 directives, connect-src 'none', no connection origins. The Trusted Types directives are not part of it; kehto adds them itself when planMount enables experimental.nestedContextLock. The profile argument is accepted and ignored; see why CSP never loosens.
renderCsp(directives: Readonly<Record<string, readonly string[]>>) => string"default-src 'none'; img-src data: blob:"-style policy string for a Content-Security-Policy meta tag.
AppletCspinterface{ connectOrigins: readonly string[]; directives: Readonly<Record<string, readonly string[]>> }, structurally compatible with kehto's NappletCspOptions.

Insertion lock ​

kernl's addition to kehto's experimental.nestedContextLock (@kehto/shell 0.30.0). kehto's lock guards frame creation; this one guards insertion, for the creation routes kehto's lock does not see (D-014).

ExportSignatureDescription
renderInsertionLockScript() => stringAn inline <script data-kernl-insertion-lock> that, run in the applet realm, applies appletInsertionLock: it pins appendChild, insertBefore, replaceChild, append, prepend, replaceChildren, before, after, replaceWith, insertAdjacentElement, moveBefore, Range.insertNode and Range.surroundContents to throw TypeError('denied:nested-frame') for any node that is, or contains (descendants, template content, shadow roots), an HTML-namespace frame element, reading node identity through native getters captured before applet code runs. It also refuses custom elements that extend frame elements and removes XSLTProcessor. Insert it immediately after kehto's namespace prelude; planMount does. A pre-frozen prototype is skipped rather than blocking the load.

Removed in favour of kehto's released lock: appletMarkupHasFrame, assertAppletHasNoFrame, renderFrameLockScript (renamed renderInsertionLockScript, now without the Trusted Types policy and factory pins), TRUSTED_TYPES_DIRECTIVES, and AppletCsp.trustedTypes.

Mitigation descriptions ​

ExportSignatureDescription
describeMitigations(config: GuardConfig) => readonly MitigationDescription[]The eleven mitigations in display order, with limit text computed from config.limits. Titles and summaries name no protocol. The table is in Guard.
MitigationDescriptioninterface{ id; title; summary; enabled: boolean; limit?: string; provider: 'kehto' | 'kernl' | 'both' }. enabled is always true in this draft.

Example ​

ts
import { applyNappletRules, createGuardConfig, createIngressGuard, describeMitigations } from '@kernl/guard';

const config = createGuardConfig('strict');
config.limits.ingressBurst; // 8
config.firewall.unfocusedMultiplier; // 0.25

const bound = applyNappletRules(config, 'chat');
bound.firewall.napplets['chat']?.rateLimits['relay:publish']?.capacity; // 20

const guard = createIngressGuard({ config, onEvent: (event) => console.log(event.reason) });
guard.admit('w1', 'chat', { type: 'relay.subscribe' }, { visible: true, focused: true }); // 'pass'

describeMitigations(config).find((m) => m.id === 'ingress-flood')?.limit; // '8 messages per second, 300 per 10s'

Scope ​

  • Pure functions and small stateful objects keyed by windowId. Call forget(windowId) on every revocation.
  • Does not parse NAP envelopes beyond type, kind and size; the shell passes those fields and may use estimateEnvelopeSize for the third.
  • Does not decide grants. Whether a domain is allowed is @kehto/acl's question, surfaced through @kernl/core.
  • Does not touch the shell's DOM or the iframe. The shell applies createAppletCsp(profile) through kehto's prepareNappletSrcdoc, with kehto's nested-context lock enabled, and planMount in @kernl/core inserts the insertion lock's script, which runs only inside the applet realm.
  • Does not carry the Trusted Types policy, the element-factory pins or the static frame scan. Those are kehto's (experimental.nestedContextLock, assertNappletHasNoFrame).
  • Does not supply initElapsedMs to kehto, so GuardConfig.firewall.burstGuard never fires inside this package. The startup burst that does fire is kehto's runtime firewall's own, at kehto's default of 20 operations in the first 3 s (Guard).

Specification ​

packages/guard/features/: profiles.feature, classification.feature, ingress.feature, visibility.feature, focus-firewall.feature, csp.feature, insertion-lock.feature, mitigations.feature.

API reference ​

Generated module: _kernl_guard

MIT licensed. Protocol authority: NIP-5D (nostr-protocol/nips PR #2303) and the NAPs (napplet/naps).