api #

fullstack app library

6 modules · 26 declarations

Modules
#

ACCOUNT_ID_KEY
#

hono_context.ts view source

"auth_account_id" import {ACCOUNT_ID_KEY} from '@fuzdev/fuz_app/hono_context.js';

Hono context variable name for the authenticated account id.

Set by the auth middleware (session, bearer, or daemon token) on a valid credential. null for unauthenticated requests. The route-spec wrapper / RPC dispatcher's authorization phase reads this when resolving the acting actor; account-grain auth guards (require_auth) and account-grain handlers read it directly.

AUTH_API_TOKEN_ID_KEY
#

hono_context.ts view source

"auth_api_token_id" import {AUTH_API_TOKEN_ID_KEY} from '@fuzdev/fuz_app/hono_context.js';

Hono context variable name for the authenticated API token id.

create_rate_limiter
#

rate_limiter.ts view source

(options?: Partial<RateLimiterOptions> | undefined): RateLimiter import {create_rate_limiter} from '@fuzdev/fuz_app/rate_limiter.js';

Create a RateLimiter with sensible defaults for per-IP login protection.

options?

override individual options; unset fields use default_login_ip_rate_limit

type Partial<RateLimiterOptions> | undefined
optional

returns

RateLimiter

CREDENTIAL_TYPE_KEY
#

hono_context.ts view source

"credential_type" import {CREDENTIAL_TYPE_KEY} from '@fuzdev/fuz_app/hono_context.js';

Hono context variable name for the credential type.

CREDENTIAL_TYPES
#

hono_context.ts view source

readonly ["session", "api_token", "daemon_token"] import {CREDENTIAL_TYPES} from '@fuzdev/fuz_app/hono_context.js';

The credential types that can authenticate a request — the closed set of fuz_app builtins. The open registry on top (create_credential_type_schema(consumer_types)) is consulted at registry time by create_role_schema for RoleSpec.required_credential_types validation; the wire-validated CredentialType enum here stays narrow because middleware only ever sets one of the three builtins.

CredentialType
#

hono_context.ts view source

ZodEnum<{ session: "session"; api_token: "api_token"; daemon_token: "daemon_token"; }> import type {CredentialType} from '@fuzdev/fuz_app/hono_context.js';

Credential type — how a request was authenticated.

default_action_account_rate_limit
#

rate_limiter.ts view source

RateLimiterOptions import {default_action_account_rate_limit} from '@fuzdev/fuz_app/rate_limiter.js';

Default options for per-actor action-dispatcher rate limiting: 1200 attempts per 15 minutes. Shared by the HTTP RPC and WebSocket action dispatchers. Permissive — sustained ~80/min is well above any human admin workflow; an oracle probing 10k addresses still finishes in ~2 hours, slow enough to surface in audit. Tighten downstream.

default_action_ip_rate_limit
#

rate_limiter.ts view source

RateLimiterOptions import {default_action_ip_rate_limit} from '@fuzdev/fuz_app/rate_limiter.js';

Default options for per-IP action-dispatcher rate limiting: 600 attempts per 15 minutes. Shared by the HTTP RPC and WebSocket action dispatchers (one budget per action, not per transport). Permissive — catches runaway scripts and egregious oracle probes, but well above human or normal automation pace. Tighten downstream for stricter deployments.

default_login_account_rate_limit
#

rate_limiter.ts view source

RateLimiterOptions import {default_login_account_rate_limit} from '@fuzdev/fuz_app/rate_limiter.js';

Default options for per-account login rate limiting: 10 attempts per 30 minutes.

default_login_ip_rate_limit
#

rate_limiter.ts view source

RateLimiterOptions import {default_login_ip_rate_limit} from '@fuzdev/fuz_app/rate_limiter.js';

Default options for per-IP login rate limiting: 5 attempts per 15 minutes.

DEFAULT_RATE_LIMITER_MAX_KEYS
#

rate_limiter.ts view source

100000 import {DEFAULT_RATE_LIMITER_MAX_KEYS} from '@fuzdev/fuz_app/rate_limiter.js';

Default tracked-key cap: bounds worst-case memory under key-enumeration attacks (an attacker rotating source IPs cannot grow the backing map indefinitely between cleanup ticks). Tuned to comfortably fit real traffic for a single-instance deployment while capping memory at a few MB in the worst case.

Email
#

primitive_schemas.ts view source

ZodString import type {Email} from '@fuzdev/fuz_app/primitive_schemas.js';

Email validation. Lives here rather than @fuzdev/fuz_util because every current consumer pairs it with Username (signup, invites, audit log) — keeping the two together avoids a cross-package import for the identity-primitive bundle. Promote to fuz_util if a non-identity consumer surfaces.

Deliberately permissive — a structural shape check (EMAIL_REGEX plus the EMAIL_LENGTH_MAX byte bound), not RFC 5322 conformance or deliverability (real delivery is proven by a confirmation email). Replaces Zod's stricter z.email() so the rule is one explicit regex the Rust spine's is_valid_email mirrors; z.email()'s internal regex (2+ char TLD, no consecutive dots) was brittle to keep in cross-impl parity and rejected addresses like a@b.c. The length bound is the UTF-8 byte count (RFC 5321 measures octets), so a multibyte address is bounded identically to the Rust spine's s.len() rather than diverging on JS's UTF-16 .length. No transform: case is preserved and surrounding whitespace is rejected (not trimmed), so storage is verbatim and the case-insensitive lookup rides the DB-side LOWER(email).

EMAIL_LENGTH_MAX
#

primitive_schemas.ts view source

254 import {EMAIL_LENGTH_MAX} from '@fuzdev/fuz_app/primitive_schemas.js';

Maximum email length in bytes — RFC 5321 §4.5.3.1.3 path-length limit (the limit is octets). Email bounds the UTF-8 byte length, matching the Rust spine's s.len() check; for an all-ASCII address bytes == characters.

generate_random_base64url
#

crypto.ts view source

(byte_length?: number): string import {generate_random_base64url} from '@fuzdev/fuz_app/crypto.js';

Generate a cryptographically random base64url string.

byte_length

number of random bytes (default 32 = 256 bits)

type number
default 32

returns

string

base64url-encoded string without padding

rate_limit_exceeded_response
#

rate_limiter.ts view source

(c: Context<any, any, {}>, retry_after: number): Response import {rate_limit_exceeded_response} from '@fuzdev/fuz_app/rate_limiter.js';

Build a 429 rate-limit-exceeded JSON response with Retry-After header.

c

Hono context

type Context<any, any, {}>

retry_after

seconds until the client should retry

type number

returns

Response

a 429 Response

RateLimiter
#

rate_limiter.ts view source

import {RateLimiter} from '@fuzdev/fuz_app/rate_limiter.js';

In-memory sliding window rate limiter.

Stores an array of timestamps per key. On check/record, timestamps outside the window are pruned. retry_after reports seconds until the oldest active timestamp expires.

The backing store is an LruMap when options.max_keys is a positive number (default DEFAULT_RATE_LIMITER_MAX_KEYS) and a plain Map when max_keys is null. The LruMap path bounds memory under key-enumeration attack at the cost of a slight per-op overhead and the LRU trade-off described on RateLimiterOptions.max_keys.

Parameters that accept RateLimiter | null (e.g. ip_rate_limiter, login_account_rate_limiter) silently disable rate limiting when null is passed — no checks are performed and all requests are allowed through.

options

type RateLimiterOptions

readonly

constructor

type new (options: RateLimiterOptions): RateLimiter

options

check

Check whether key is allowed without recording an attempt.

Prunes timestamps that fell outside the window as a side effect (and removes the key entirely when none remain), so the backing map stays bounded even under read-only traffic.

type (key: string, now?: number): RateLimitResult

key

rate limit key (e.g. IP address)

type string

now

current timestamp in ms (defaults to Date.now())

type number
default Date.now()

record

Record a failed attempt for key and return the updated result.

type (key: string, now?: number): RateLimitResult

key

rate limit key (e.g. IP address)

type string

now

current timestamp in ms (defaults to Date.now())

type number
default Date.now()

reset

Clear all attempts for key (e.g. after successful login).

type (key: string): void

key

type string
returns void

cleanup

Remove entries whose timestamps are all outside the window.

type (now?: number): void

now

current timestamp in ms (defaults to Date.now())

type number
default Date.now()
returns void

dispose

Stop the cleanup timer. Safe to call multiple times.

type (): void

returns void

size

Number of tracked keys.

type number

getter

RateLimiterOptions
#

rate_limiter.ts view source

RateLimiterOptions import type {RateLimiterOptions} from '@fuzdev/fuz_app/rate_limiter.js';

Configuration for a rate limiter instance.

max_attempts

Maximum allowed attempts within the window.

type number

window_ms

Sliding window duration in milliseconds.

type number

cleanup_interval_ms

Interval for pruning stale entries (0 disables the timer).

type number

max_keys?

Maximum tracked keys. When exceeded, the least-recently-used key is evicted — bounds memory under key-enumeration attacks. Default: DEFAULT_RATE_LIMITER_MAX_KEYS (100_000). Pass null to disable the cap (falls back to an unbounded Map — only recommended when the key set is known to be closed, e.g. a per-account limiter keyed to a bounded-size account table).

LRU trade-off: every check / record call marks the key as most-recently-used, so keys under active attack stay fresh and won't be evicted. A slow-burn attacker spread across many low-volume keys can, however, drop out of the table and reset their budget — set max_keys high enough to fit the expected legitimate key set and this stays theoretical.

type number | null

RateLimitResult
#

rate_limiter.ts view source

RateLimitResult import type {RateLimitResult} from '@fuzdev/fuz_app/rate_limiter.js';

Result of a rate limit check or record operation.

allowed

Whether the request is allowed.

type boolean

remaining

Remaining attempts before blocking.

type number

retry_after

Seconds until the oldest active attempt expires (0 if allowed).

type number

SchemaFieldMeta
#

schema_meta.ts view source

SchemaFieldMeta import type {SchemaFieldMeta} from '@fuzdev/fuz_app/schema_meta.js';

Zod .meta() shape for fuz_app schema metadata conventions.

description?

type string

sensitivity?

Sensitivity level for masking/redaction. 'secret' masks the value.

type Sensitivity

Sensitivity
#

sensitivity.ts view source

"secret" import type {Sensitivity} from '@fuzdev/fuz_app/sensitivity.js';

Sensitivity level for a schema field.

  • 'secret' — value is masked in logs and UI (e.g. passwords, API keys, signing keys)

TEST_CONTEXT_PRESET_KEY
#

hono_context.ts view source

"test_context_preset" import {TEST_CONTEXT_PRESET_KEY} from '@fuzdev/fuz_app/hono_context.js';

Hono context variable name for the test-harness pre-baked context flag.

Test harnesses (create_test_app_from_specs, create_fake_hono_context, the WS round-trip connect() helper, plus per-test middleware that pre-populates REQUEST_CONTEXT_KEY) set this to true so apply_authorization_phase skips its DB-backed actor resolution and trusts the supplied RequestContext. Production middleware never sets this key — only test code does. The flag is the explicit escape hatch that replaced the implicit "is REQUEST_CONTEXT_KEY already set?" probe, so that future production code consulting REQUEST_CONTEXT_KEY cannot silently bypass the live build.

Username
#

primitive_schemas.ts view source

ZodPipe<ZodString, ZodTransform<string, string>> import type {Username} from '@fuzdev/fuz_app/primitive_schemas.js';

Username for account creation — starts with letter, alphanumeric/dash/underscore middle, ends with alphanumeric. No @ or . allowed.

Canonicalized to lowercase at parse time. The regex rejects whitespace outright, so .trim() is unnecessary here. Storage is canonical across every creation site (bootstrap, signup, admin-create, invite acceptance) because the schema is the single source of truth — eliminates the per-caller trim().toLowerCase() ritual and keeps the LOWER(username) = LOWER($1) lookup contract simple.

USERNAME_LENGTH_MAX
#

primitive_schemas.ts view source

39 import {USERNAME_LENGTH_MAX} from '@fuzdev/fuz_app/primitive_schemas.js';

Maximum username length (matches GitHub's limit).

USERNAME_LENGTH_MIN
#

primitive_schemas.ts view source

3 import {USERNAME_LENGTH_MIN} from '@fuzdev/fuz_app/primitive_schemas.js';

Minimum username length (must have start + middle + end characters).

USERNAME_PROVIDED_LENGTH_MAX
#

primitive_schemas.ts view source

255 import {USERNAME_PROVIDED_LENGTH_MAX} from '@fuzdev/fuz_app/primitive_schemas.js';

Maximum length for username input on login/lookup — more permissive than USERNAME_LENGTH_MAX for forward-compatibility if the creation limit is raised.

UsernameProvided
#

primitive_schemas.ts view source

ZodPipe<ZodString, ZodTransform<string, string>> import type {UsernameProvided} from '@fuzdev/fuz_app/primitive_schemas.js';

Username submitted for login or lookup — minimal validation for forward-compatibility if format rules change.

Canonicalized via .trim().toLowerCase() at parse time so login's per-account rate-limit key and DB lookup see a uniform value regardless of casing or surrounding whitespace. Mirrors the storage canonicalization on Username so submission and storage agree.

The trailing .refine rejects a whitespace-only identifier: .min(1) runs on the raw string (so " " passes it), and without the post-trim check the value would canonicalize to "" and fall through to a lookup-miss 401 instead of a 400 — an empty identifier is malformed input, not a wrong credential. Keeps the Rust spine's account_login (which rejects empty-after-trim) in parity.