Chobble Tickets

A self-hosted ticket reservation system built on Deno with libsql.

Features

  • Free and paid events (Stripe/Square integration)
  • Standard events (fixed capacity) and daily events (date-based booking)
  • Multi-event bookings in one checkout
  • Hybrid RSA-OAEP + AES-256-GCM encryption for PII at rest
  • Check-in with QR codes (built-in scanner)
  • Apple Wallet pass support
  • Admin dashboard with multi-user management
  • Email notifications (Resend, Postmark, SendGrid, Mailgun)
  • Public JSON API (optional)
  • Webhooks on registration
  • ICS/RSS calendar feeds
  • Embeddable widget via iframe

Modules

Module Description
Database ORM, table abstractions, and entity CRUD
Crypto Encryption, hashing, and CSRF
Payments Stripe and Square integration
Email Email sending and templates
Tickets QR codes, SVG tickets, Apple Wallet
Events Event fields, sorting, availability
Config Settings, environment, sessions
Utilities FP helpers, formatting, caching
Embed Widget embedding and CDN
Webhooks Webhook delivery and API examples
Demo Demo mode and seed data

Deployment Options

  • Bunny Edge Scripting (edge-deployed)
  • Docker containers
  • Any Deno-compatible environment

Classes

c
PaymentUserError(message: string)

Error subclass for user-facing payment validation errors (e.g. invalid phone number). These propagate through safeAsync so the message can be shown to the user.

Functions

f
addDays(
dateStr: string,
days: number
): string

Add days to a YYYY-MM-DD date string

f
addPendingWork(p: Promise<unknown>): void

Queue a promise that must complete before the response is sent

f
addQueryLogEntry(
sql: string,
durationMs: number
): void

Record a query (no-op when logging is disabled)

f
aesGcmDecryptRaw(
iv: Uint8Array,
ciphertext: Uint8Array,
key: CryptoKey
): Promise<ArrayBuffer>

AES-GCM decrypt raw data, returning the decrypted ArrayBuffer

f
appendIframeParam(url: string): string

Append iframe=true query param to a URL when in iframe mode

f
applyDemoOverrides(
form: FormParams,
mapping: DemoFieldMap
): FormParams

Replace form field values with demo data when demo mode is active. Only replaces fields that are present and non-empty in the form. Mutates and returns the same URLSearchParams for chaining.

f
assignEventsToGroup(
eventIds: number[],
groupId: number
): Promise<void>

Assign events to a group by updating their group_id.

f
asString(value: unknown): string

Narrow an unknown value to string, defaulting to "" if not a string. Replaces typeof x === "string" ? x : "" at type boundaries.

f
base64ToBase64Url(b64: string): string

Convert standard base64 to base64url (no padding). Works on both strings and Uint8Array (bytes are first encoded to base64).

f
bracket<R>(
acquire: () => R | Promise<R>,
release: (r: R) => void | Promise<void>
)

Resource management pattern (like Haskell's bracket or try-with-resources). Ensures cleanup happens even if the operation throws.

f
buildAttendeeInsert(enc: EncryptedAttendeeData)

Build an INSERT statement for the attendees table from encrypted fields.

f
buildCheckinUrl(token: string): string

Build the check-in URL for a single ticket token

f
buildEmbedSnippets(url: string): EmbedSnippets

Build embed snippets (script and iframe variants) for a ticket URL

f
buildFlashCookie(
id: string,
message: string,
succeeded: boolean,
result?: string
): string

Build a flash cookie containing a success or error message, keyed by ID

f
buildFrameAncestors(hosts: string[]): string | null

Build a frame-ancestors CSP value from allowed embed hosts. Returns null if the list is empty (allow embedding from anywhere).

f
buildInfoLines(data: SvgTicketData): string[]

Build info lines from ticket data (non-PII event and booking details)

f
buildInputKeyMap(columns: string[]): Record<string, string>

Build input key mapping from DB columns snake_case DB column → camelCase input key

f
buildItemsMetadata(intent: CheckoutIntent): Record<string, string>

Build checkout metadata from a CheckoutIntent (converts items to compact form).

f
buildMetadata(intent: MetadataInput): Record<string, string>

Build checkout session metadata from booking data (items already compact).

f
buildPkpass(
data: PassData,
creds: SigningCredentials
): Uint8Array

Build a complete .pkpass file as a Uint8Array (ZIP archive)

f
buildSubdomainRecordName(subdomain: string): string

Build the full subdomain record name (user choice + suffix). e.g. "myevent" + ".tickets" → "myevent.tickets"

f
buildSvgTicketData(
entry: EmailEntry,
currency: string
): SvgTicketData

Build SVG ticket data from an email entry (non-PII only)

f
buildWebhookPayload(
entries: RegistrationEntry[],
currency: string
): WebhookPayload

Build a consolidated webhook payload from registration entries

f
checkBatchAvailability(
items: BatchAvailabilityItem[],
date?: string | null
): Promise<boolean>

Wrapper for test mocking - delegates to attendeesApi at runtime

f
checkCapacityResult(result: { rowsAffected: number; }): UpdateEventLinkResult

Check a capacity-guarded write result and invalidate cache on success

f
checkSubdomainAvailable(subdomain: string)

Check whether a bunny subdomain is available.

f
chunk(size: number)

Split an array into chunks of a given size

f
clearFlashCookie(id: string): string

Clear a keyed flash cookie (set after reading)

f
clearLoginAttempts(ip: string): Promise<void>

Clear login attempts for an IP (on successful login)

f
clearSessionTokens(sessionId: string): Promise<void>

Clear stored ticket tokens for a session (after redirect has consumed them)

f
collectionCache<T>(
fetchAll: () => Promise<T[]>,
ttlMs: number,
now?: () => number
): CollectionCache<T>

Create an in-memory collection cache with TTL. Loads all items via fetchAll on first access or after invalidation/expiry, then serves from memory until the TTL expires or invalidate() is called. Accepts an optional clock function for testing.

f
compact<T>(array: (T | null | undefined)[]): T[]

Remove null and undefined values from array

f
computeGroupSlugIndex(slug: string): Promise<string>

Compute slug index from slug for blind index lookup

f
computeHmacSha256(
data: Uint8Array,
secret: string
): Promise<ArrayBuffer>

Compute HMAC-SHA256 using Web Crypto API, returning raw ArrayBuffer

f
computeSlugIndex(slug: string): Promise<string>

Compute slug index from slug for blind index lookup

f
computeTicketTokenIndex(token: string): Promise<string>

Compute ticket token index using HMAC for blind lookups Similar to slug_index for events - allows lookup without decrypting

f
constantTimeEqual(
a: string,
b: string
): boolean

Constant-time string comparison to prevent timing attacks Always iterates over the longer string and XORs the lengths so that different-length inputs don't leak via an early return.

f
contactFields(unnamed 0: ContactInfo): ContactInfo

Extract ContactInfo fields from an object

f
createAttendeeAtomic(input: AttendeeInput): Promise<CreateAttendeeResult>

Wrapper for test mocking - delegates to attendeesApi at runtime

f
createManifest(files: Record<string, Uint8Array>): string

Create manifest.json mapping filenames to SHA-1 hashes

f
createRequestTimer(): (() => number)

Create a request timer for measuring duration

f
createSeeds(
eventCount: number,
attendeesPerEvent: number
): Promise<SeedResult>

Create seed events and attendees using efficient batch writes. Encrypts all data before inserting, matching production behavior. Assigns random ticket quantities (1-4) per attendee without overselling.

f
createSession(
token: string,
csrfToken: string,
expires: number,
wrappedDataKey: string | null,
userId: number
): Promise<void>

Create a new session with CSRF token, wrapped data key, and user ID Token is hashed before storage for security

f
createTypeGuard<T extends string>(values: readonly T[])

Create a type guard from a readonly array of string literal values

f
createWithClient<Client>(getClient: () => Client | null | Promise<Client | null>)

Create a withClient helper that runs an operation with a lazily-resolved client. Returns null if the client is not available or the operation fails.

f
dateToStartEnd(date: string | null): { startAt: string | null; endAt: string | null; }

Convert nullable date to start_at/end_at (null-safe wrapper around dateToRange)

f
daysAgo(utcIso: string): number | null

Compute how many days ago an event started, relative to today in the configured timezone. Returns null if the event date is today or in the future, or if the date is empty/invalid. For past events, returns a positive integer (1 = yesterday).

f
decrypt(encrypted: string): Promise<string>

Decrypt a string value encrypted with encrypt() Expects format: enc:1:$base64iv:$base64ciphertext

f
decryptAttendeeFields(
row: Attendee,
privateKey: CryptoKey,
paidEvent?: boolean
): Promise<Attendee>

Decrypt attendee fields from the PII blob. Requires migration to be complete (admin is gated behind migration). When paidEvent is false, payment_id and refunded are skipped.

f
decryptAttendeeOrNull(
row: Attendee | null,
privateKey: CryptoKey
): Promise<Attendee | null>

Decrypt a single raw attendee, handling null input. Used when attendee is fetched via batch query.

f
decryptAttendeePII(
encrypted: string,
privateKey: CryptoKey
): Promise<string>

Decrypt attendee PII using the private key Used in admin views after obtaining private key from session

f
decryptAttendees(
rows: Attendee[],
privateKey: CryptoKey,
paidEvent?: boolean
): Promise<Attendee[]>

Decrypt a list of raw attendees (all fields). Used when attendees are fetched via batch query.

f
decryptBytes(encrypted: Uint8Array): Promise<Uint8Array>

Decrypt binary data encrypted with encryptBytes(). Expects ENCB binary format: magic + version + IV + ciphertext.

f
decryptSessionTokens(encryptedTokens: string): Promise<string>

Decrypt the ticket_tokens field from a processed payment record. Returns the plaintext token string (e.g. "tok1+tok2") or empty string.

f
defineIdTable<Row, Input = Row>(
name: string,
schema: TableSchema<Row>
)

Helper for tables whose primary key column is id.

f
deleteAllEventStorageFiles(events: ReadonlyArray<EventWithStorage>): Promise<void>

Delete all storage files (images and attachments) for a list of events

f
deleteAllSessions(): Promise<void>

Delete all sessions (used when password is changed)

f
deleteAllStaleReservations(): Promise<number>

Delete all stale reservations (unfinalized and older than STALE_RESERVATION_MS). Called from admin event views to clean up abandoned checkouts.

f
deleteAttendee(attendeeId: number): Promise<void>

Delete an attendee and all its event links, payments, and answers.

f
deleteEvent(eventId: number): Promise<void>

Delete an event and all its attendees in a single database round-trip. Uses write batch to cascade: processed_payments → attendees → event. Reduces 3 sequential HTTP round-trips to 1.

f
deleteEventStorageFiles(
event: EventWithStorage,
reason: string
): Promise<void>

Delete the image and attachment files for a single event

f
deleteFile(filename: string): Promise<void>

Delete a file, routing to local or Bunny based on config.

f
deleteOtherSessions(currentToken: string): Promise<void>

Delete all sessions except the current one Token is hashed before database comparison

f
deleteSession(token: string): Promise<void>

Delete a session by token Token is hashed before database lookup

f
deleteStaleReservation(sessionId: string): Promise<void>

Delete a stale reservation to allow retry

f
deleteUser(userId: number): Promise<void>

Delete a user and all their sessions and API keys

f
deployScriptCode(code: string): Promise<BunnyApiResult>

Upload and publish new script code to Bunny CDN.

f
deriveKEK(passwordHash: string): Promise<CryptoKey>

Derive a Key Encryption Key (KEK) from password hash and DB_ENCRYPTION_KEY Uses PBKDF2 with the password hash as input and DB_ENCRYPTION_KEY as salt

f
detectIframeMode(url: string): void

Detect iframe mode from a request URL and store it for the current request

f
detectImageType(data: Uint8Array): string | null

Detect the actual image type from magic bytes. Returns the MIME type if matched, null otherwise.

f
downloadImage(filename: string): Promise<Uint8Array | null>

Download and decrypt a file. Returns the decrypted bytes, or null if the file does not exist.

f
downloadRaw(filename: string): Promise<Uint8Array | null>

Download raw bytes from storage. Returns null if the file does not exist.

f
enableQueryLog(): void

Enable query logging and clear previous entries

f
encrypt(plaintext: string): Promise<string>

Encrypt a string value using AES-256-GCM via Web Crypto API Returns format: enc:1:$base64iv:$base64ciphertext Note: ciphertext includes auth tag appended (Web Crypto API does this automatically)

f
encryptAttendeeFields(input: EncryptInput): Promise<EncryptedAttendeeData | null>

Encrypt attendee fields into a PII blob, returning null if key not configured

f
encryptAttendeePII(
plaintext: string,
publicKeyJwk: string
): Promise<string>

Encrypt attendee PII using the public key from settings This can be called without authentication (for public ticket forms)

f
encryptBytes(data: Uint8Array): Promise<Uint8Array>

Encrypt binary data with AES-256-GCM using compact binary format. Output: ENCB + version byte + 12-byte IV + ciphertext (with GCM auth tag). Overhead is only 33 bytes (vs ~76% bloat in the legacy text format).

f
encryptedNameSchema(
encrypt: EncryptFn,
decrypt: DecryptFn
)

Shared encrypted name column for tables that store a display name.

f
encryptPiiBlob(
blobJson: string,
publicKeyJwk: string
): Promise<string>

Encrypt a PII blob JSON string with the public key

f
encryptWithKey(
plaintext: string,
key: CryptoKey
): Promise<string>

Encrypt data with a symmetric key (for wrapping private key with DATA_KEY)

f
errorMessage(err: unknown): string

Extract a human-readable message from an unknown caught value

f
eventDateToCalendarDate(utcIso: string): string | null

Convert a UTC ISO datetime to a YYYY-MM-DD calendar date in the given timezone. Returns null if the input is empty or invalid. Used by the calendar view to map standard event dates to calendar days.

f
eventSupportsDirectCheckout(event: Pick<EventWithCount, "id" | "fields">): Promise<boolean>

Whether this event can send QR code scanners directly to checkout. True when no extra contact fields or questions are required.

f
executeBatch(statements: Array<{ sql: string; args: InValue[]; }>): Promise<void>

Execute multiple write statements, discarding results.

f
extractSessionMetadata(metadata: SessionMetadata): ValidatedPaymentSession["metadata"]

Normalize validated session metadata into the canonical SessionMetadata shape.

f
extractSvgContent(svg: string): string

Extract the inner content of an SVG element (strip the outer <svg> wrapper)

f
extractViewBox(svg: string): { width: number; height: number; }

Extract the viewBox from an SVG element to compute its coordinate space

f
finalizeSession(
sessionId: string,
attendeeId: number,
ticketTokens?: string[]
): Promise<void>

Finalize a reserved session with the created attendee ID (second phase)

f
flushPendingWork(): Promise<void>

Await all queued work. Call before returning the response.

f
formatCurrency(minorUnits: number | string): string

Format an amount in minor units (pence/cents) as a currency string. e.g. formatCurrency(1050) → "£10.50" (when currency is GBP)

f
formatDateLabel(dateStr: string): string

Format a YYYY-MM-DD date for display. Returns "Monday 15 March 2026"

f
formatDatetimeInTz(
utcIso: string,
tz: string
): string

Format a UTC ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"

f
formatDatetimeLabel(iso: string): string

Format an ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"

f
formatDatetimeShort(iso: string): string

Compact ISO datetime formatter for table cells. Returns e.g. "07/04/2026 14:00" in the configured timezone.

f
formatDatetimeShortInTz(
utcIso: string,
tz: string
): string

Compact format for table cells: "yyyy-MM-dd HH:mm" in the given timezone. Delegates to the browser-compatible formatIsoForPreview helper so the same formatting runs on the server and in the admin JS bundle.

f
formatErrorMessage(context: ErrorContext): string

Format an error context into a human-readable activity log message

f
formatPrefixed(
prefix: string,
...parts: Uint8Array[]
): string

Format IV + ciphertext as a prefixed base64 string

f
formatRequestError(
method: string,
path: string,
error: unknown
): string

Format an error detail string with request context and error message

f
fromBase64(base64: string): Uint8Array

Convert base64 string to Uint8Array

f
generateAttachmentFilename(originalName: string): string

Generate a random CDN filename preserving the original name for readability

f
generateDataKey(): Promise<CryptoKey>

Generate a random 256-bit symmetric key for data encryption

f
generateImageFilename(detectedType: string): string

Generate a random filename with the correct extension

f
generateKeyPair(): Promise<{ publicKey: string; privateKey: string; }>

Generate an RSA key pair for asymmetric encryption Returns { publicKey, privateKey } as exportable JWK strings

f
generatePassJson(
data: PassData,
creds: SigningCredentials
): Record<string, unknown>

Build the pass.json content from pass data and signing credentials

f
generateQrSvg(text: string): string

Generate an SVG string for a QR code encoding the given text. Returns a complete <svg> element suitable for inline embedding.

f
generateSecureToken(): string

Generate a cryptographically secure random token Uses Web Crypto API getRandomValues

f
generateSlug(): string

Generate a random slug with at least 2 digits and 2 letters. Uses Fisher-Yates shuffle on the fixed positions to avoid bias.

f
generateSvgTicket(data: SvgTicketData): Promise<string>

Generate a standalone SVG ticket with QR code and event/booking details. Returns a complete SVG document string.

f
generateTicketToken(): string

Generate a 5-byte uppercase hex ticket token for public ticket URLs

f
getActiveEventsByGroupId(groupId: number): Promise<EventWithCount[]>

Get active events in a group with attendee counts.

f
getActiveEventStats(events: EventWithCount[]): Promise<ActiveEventStats>

Get aggregated statistics for active events. Filters active events from the provided list, computes attendees (sum of quantities) from cached EventWithCount data, and queries ticket count and income (sum of price_paid) via a single aggregate.

f
getActiveHolidays(): Promise<Holiday[]>

Get active holidays (end_date >= today) for date computation (from cache). "today" is computed in the configured timezone.

f
getActivePaymentProvider(): Promise<PaymentProvider | null>

Resolve the active payment provider based on admin settings. Lazy-loads the provider module to avoid importing unused SDKs. Returns null if no provider is configured.

f
getAllActivityLog(limit?: number): Promise<ActivityLogEntry[]>

Get all activity log entries (most recent first)

f
getAllCacheStats(): CacheStat[]

Collect stats from all registered caches

f
getAllDailyEvents(): Promise<EventWithCount[]>

Get all daily events with attendee counts (from cache).

f
getAllEvents(): Promise<EventWithCount[]>

Get all events with attendee counts (from cache)

f
getAllGroups(): Promise<Group[]>

Get all groups, decrypted, ordered by id (from cache)

f
getAllHolidays(): Promise<Holiday[]>

Get all holidays, decrypted, ordered by start_date (from cache)

f
getAllSessions(): Promise<Session[]>

Get all sessions ordered by expiration (newest first)

f
getAllStandardEvents(): Promise<EventWithCount[]>

Get all standard events with attendee counts (from cache). Used by the calendar view to include one-time events on their scheduled date.

f
getAllUsers(): Promise<User[]>

Get all users (for admin user management page, from cache)

f
getAttendee(
id: number,
privateKey: CryptoKey
): Promise<Attendee | null>

Get an attendee by ID (decrypted) Requires private key for decryption - only available to authenticated sessions

f
getAttendeeRaw(id: number): Promise<Attendee | null>

Get an attendee by ID without decrypting PII Used for payment callbacks and webhooks where decryption is not needed Returns the attendee with encrypted fields (id, event_id, quantity are plaintext)

f
getAttendeesByEventIds(eventIds: number[]): Promise<Attendee[]>

Get raw attendees for a set of event IDs. Used by the calendar to load attendees for standard events whose decrypted date matches the selected calendar date.

f
getAttendeesByTokens(tokens: string[]): Promise<(AttendeeWithBookings | null)[]>

Look up attendees by plaintext tokens, returning full booking data. Two queries: attendees by token index, then all event_attendees for those attendees. Returns results in the same order as input tokens (deduped). Bookings sorted by start_at then event_id for deterministic ordering.

f
getAttendeesRaw(eventId: number): Promise<Attendee[]>

Get attendees for an event without decrypting PII Used for tests and operations that don't need decrypted data

f
getAvailableDates(
event: Event,
holidays: Holiday[]
): string[]

Compute available booking dates for a daily event. Filters by bookable days of the week and excludes holidays. Returns sorted array of YYYY-MM-DD strings.

f
getBookingFee(): number

Get booking fee percentage from database. Returns 0 if not set.

f
getBunnyApiKey(): string

Get the Bunny CDN API key from environment

f
getBunnyDnsSubdomainSuffix(): string

Get the Bunny DNS subdomain suffix (e.g. ".tickets") from environment

f
getBunnyDnsZoneId(): string

Get the Bunny DNS zone ID from environment

f
getBunnyScriptId(): string

Get the Bunny Edge Script ID from environment

f
getCachedSession(): AuthSession | null | undefined

Return the cached session if already resolved, or undefined if not yet resolved

f
getCdnHostname(): Promise<CdnHostnameResult>

Get CDN hostname (delegates to bunnyCdnApi for testability).

f
getCurrentCsrfToken(): string

Get the most recently generated CSRF token (for synchronous JSX rendering)

f
getDailyEventAttendeeDates(): Promise<string[]>

Get distinct attendee dates for daily events. Used for the calendar date picker (lightweight, no attendee data).

f
getDailyEventAttendeesByDate(date: string): Promise<Attendee[]>

Get raw attendees for daily events on a specific date. Bounded query: only returns attendees matching the given date.

f
getDateAttendeeCount(
eventId: number,
date: string
): Promise<number>

Get the total attendee quantity for a specific event + date

f
getDb(): Client

Get or create database client

f
getDecimalPlaces(currencyCode: string): number

Get the number of decimal places for a currency code

f
getEffectiveDomain(): string

Get the effective domain synchronously (must call loadEffectiveDomain first).

f
getEmailConfig(): EmailConfig | null

Read email config from DB settings. Falls back to business email for fromAddress. Returns null if not configured.

f
getEmbedHosts(): Promise<string[]>

Get allowed embed hosts from database (encrypted, parsed to array) Returns empty array if not configured (embedding allowed from anywhere)

f
getEncryptionKeyString(): string

Get the encryption key bytes from environment variable (sync validation only) Expects DB_ENCRYPTION_KEY to be a base64-encoded 256-bit (32 byte) key

f
getEnv(key: string): string | undefined

Get an environment variable value Checks process.env first (Bunny Edge), falls back to Deno.env (local dev)

f
getEvent(id: number): Promise<Event | null>

Get a single event by ID (from cache)

f
getEventActivityLog(
eventId: number,
limit?: number
): Promise<ActivityLogEntry[]>

Get activity log entries for an event (most recent first)

f
getEventsByGroupId(groupId: number): Promise<EventWithCount[]>

Get all events in a group with attendee counts (including inactive).

f
getEventsBySlugsBatch(slugs: string[]): Promise<(EventWithCount | null)[]>

Get multiple events by slugs (from cache). Returns events in the same order as the input slugs. Missing or inactive events are returned as null.

f
getEventWithActivityLog(
eventId: number,
limit?: number
): Promise<EventWithActivityLog | null>

Get event and its activity log in a single database round-trip. Uses batch API to reduce latency for remote databases.

f
getEventWithAttendeeRaw(
eventId: number,
attendeeId: number
): Promise<EventWithAttendeeRaw | null>

Get event and a single attendee in a single database round-trip. Used for attendee management pages where we need both the event context and the specific attendee data.

f
getEventWithAttendeesRaw(id: number): Promise<EventWithAttendees | null>

Get event and all attendees in a single database round-trip. Uses batch API to execute both queries together, reducing latency for remote databases like Turso from 2 RTTs to 1. Computes attendee_count from the attendees array.

f
getEventWithCount(id: number): Promise<EventWithCount | null>

Get event with attendee count (from cache)

f
getEventWithCountBySlug(slug: string): Promise<EventWithCount | null>

Get event with attendee count by slug (from cache)

f
getGroupBySlugIndex(slugIndex: string): Promise<Group | null>

Get a single group by slug_index (from cache)

f
getGroupRemainingByEventId(
events: EventForGroupLookup[],
date?: string | null
): Promise<RemainingMap>

Per-event view of group remaining capacity. Daily events are dropped when date is null — their cap is per-date, so a cumulative count would misreport spots that other dates still have.

f
getGroupRemainingByGroupId(
groupIds: number[],
date?: string | null
): Promise<RemainingMap>

Per-group remaining capacity. Groups with max_attendees <= 0 (no cap) are omitted from the map. With date = null, daily-event attendees count cumulatively — correct for booking-time enforcement after upstream date validation, misleading for display.

f
getGroupRemainingForEvent(
event: EventForGroupLookup,
date?: string | null
): Promise<number | undefined>

Returns undefined when no group cap applies: ungrouped, uncapped group, or daily event without a date.

f
getHostEmailConfig(): EmailConfig | null

Get host-level email config. Uses test override if set, otherwise reads env vars.

f
getIframeMode(): boolean

Get the current request's iframe mode

f
getImageProxyUrl(filename: string): string

Get the proxy URL path for serving a decrypted image. Images are encrypted on CDN, so they must be served through the proxy.

f
getMimeTypeFromFilename(filename: string): string | null

Get the MIME type for an image filename from its extension.

f
getNewestAttendeesRaw(limit: number): Promise<Attendee[]>

Get the newest attendees across all events without decrypting PII. Used for the admin dashboard to show recent registrations.

f
getNextBookableDate(
event: Event,
holidays: Holiday[]
): string | null

Get the next available booking date for a daily event. More efficient than getAvailableDates()[0] — stops at first match. Returns null if no bookable dates are available.

f
getPrivateKeyFromSession(
sessionToken: string,
wrappedDataKey: string,
wrappedPrivateKey: string
): Promise<CryptoKey>

Derive the private key from session credentials Used to decrypt attendee PII in admin views Results are cached per session token for 10 seconds

f
getProcessedAttendeeId(sessionId: string): Promise<number | null>

Get the attendee ID for an already-processed session Used to return success for idempotent webhook retries

f
getQueryLog(): QueryLogEntry[]

Return a snapshot of all logged queries

f
getQueryLogStartTime(): number

Return the start time recorded by enableQueryLog()

f
getRandomBytes(length: number): Uint8Array

Generate random bytes using Web Crypto API

f
getRequestId(): string

Get the current request ID, or empty string if outside request context

f
getSession(token: string): Promise<Session | null>

Get a session by token (with 10s TTL cache) Token is hashed for database lookup

f
getStorageBackend(): "bunny" | "local" | "none"

Returns which storage backend is active: "bunny", "local", or "none".

f
getUngroupedEvents(): Promise<EventWithCount[]>

Get ungrouped events (group_id = 0) with attendee counts.

f
f
getUserByInviteCode(inviteCode: string): Promise<User | null>

Find a user by invite code hash Scans all users, decrypts invite_code_hash, and compares

f
getUserByUsername(username: string): Promise<User | null>

Look up a user by username (using blind index, from cache)

f
hasAvailableSpots(...args: Parameters<attendeesApi.hasAvailableSpots>): Promise<boolean>

Wrapper for test mocking - delegates to attendeesApi at runtime

f
hashInviteCode(code: string): Promise<string>

Hash an invite code using SHA-256

f
hashPassword(password: string): Promise<string>

Hash a password using PBKDF2 Returns format: pbkdf2:iterations:$base64salt:$base64hash

f
hashSessionToken(token: string): Promise<string>

Hash a session token using SHA-256 Used to store session lookups without exposing the actual token

f
hasPassword(user: User): Promise<boolean>

Check if a user has set their password (password_hash is non-empty encrypted value)

f
hasPendingWorkScope(): boolean

True when running inside a runWithPendingWork scope (i.e. a request).

f
hmacHash(value: string): Promise<string>

HMAC-SHA256 hash using DB_ENCRYPTION_KEY Used for blind indexes and hashing limited keyspace values Returns deterministic output for same input (unlike encrypt)

f
hmacToBase64(buf: ArrayBuffer): string

Convert ArrayBuffer to base64 string

f
hmacToHex(buf: ArrayBuffer): string

Convert ArrayBuffer to hex string

f
hybridDecrypt(
encrypted: string,
privateKey: CryptoKey
): Promise<string>

Decrypt data using hybrid encryption Expects format: hyb:1:$base64WrappedKey:$base64iv:$base64ciphertext Results are cached in a bounded LRU (ciphertext -> plaintext)

f
hybridEncrypt(
plaintext: string,
publicKey: CryptoKey
): Promise<string>

Encrypt data using hybrid encryption (RSA + AES)

f
idAndEncryptedSlugSchema(
encrypt: EncryptFn,
decrypt: DecryptFn
)

Shared columns for tables with encrypted slug + blind-index slug_index.

f
f
f
incrementAttachmentDownloads(
attendeeId: number,
eventId: number
): Promise<void>

Increment the attachment download counter for an attendee. Uses atomic SQL increment to avoid race conditions.

f
initDb(): Promise<void>

Initialize database tables — idempotent, safe to call on every startup. Uses an advisory lock to prevent concurrent migrations.

f
inPlaceholders(values: readonly unknown[]): string

Build SQL placeholders for an IN clause, e.g. "?, ?, ?"

f
insert(
table: string,
values: Record<string, InValue | RawSql>
): { sql: string; args: InValue[]; }

Build an INSERT statement from a table name and column→value record.

f
invalidateEventsCache(): void

Invalidate the events cache (for testing or after writes).

f
invalidateGroupsCache(): void

Invalidate the groups cache (for testing or after writes).

f
invalidateHolidaysCache(): void

Invalidate the holidays cache (for testing or after writes).

f
invalidateUsersCache(): void

Invalidate the users cache (for testing or after writes).

f
isBunnyCdnEnabled(): boolean

Check if Bunny CDN pull zone management is enabled Requires both BUNNY_API_KEY and BUNNY_SCRIPT_ID to be set

f
isBunnyDnsEnabled(): boolean

Check if Bunny DNS subdomain feature is enabled. Requires BUNNY_API_KEY and BUNNY_DNS_ZONE_ID to be set.

f
isDemoMode(): boolean

Check if demo mode is enabled

f
isEmailProvider(value: string): value is EmailProvider

Type guard: checks if a string is a valid EmailProvider

f
isGroupSlugTaken(
slug: string,
excludeGroupId?: number
): Promise<boolean>

Check if a group slug is already in use. Checks both events and groups for cross-table uniqueness.

f
isInviteExpired(user: User): Promise<boolean>

Check if a user's invite has expired. Callers should skip this for users who have already set a password.

f
isInviteValid(user: User): Promise<boolean>

Check if a user's invite is still valid (not expired, has invite code)

f
isLoginRateLimited(ip: string): Promise<boolean>

Check if IP is rate limited for login

f
isPaidEvent(event: Pick<Event, "unit_price" | "can_pay_more">): boolean

Whether an event can accept payments (has a price or allows pay-what-you-want)

f
isPaymentsEnabled(): boolean

Check if payments are enabled (any provider configured with valid keys)

f
isPaymentStatus(s: string): s is PaymentStatus

Type guard: check if a string is a valid PaymentStatus

f
isQueryLogEnabled(): boolean

Whether query logging is currently active

f
isReadOnly(): boolean

Check if the system is in read-only mode (READ_ONLY env var)

f
isReservationStale(processedAt: string): boolean

Check if a reservation is stale (abandoned by a crashed process)

f
isSessionProcessed(sessionId: string): Promise<ProcessedPayment | null>

Check if a payment session has already been processed

f
isSignedCsrfToken(token: string): boolean

Check whether a token uses the signed format

f
isSlugTaken(
slug: string,
excludeEventId?: number
): Promise<boolean>

Check if a slug is already in use (optionally excluding a specific event ID) Uses slug_index for lookup (blind index)

f
isStorageEnabled(): boolean

Check if image storage is enabled (Bunny CDN or local filesystem).

f
isUsernameTaken(username: string): Promise<boolean>

Check if a username is already taken

f
isValidDatetime(value: string): boolean

Check if a naive datetime-local string is a parseable datetime. Does not interpret timezone — purely a format check.

f
isValidPemCertificate(pem: string): boolean

Validate that a string is a parseable PEM certificate

f
isValidPemPrivateKey(pem: string): boolean

Validate that a string is a parseable PEM private key

f
isValidTimezone(tz: string): boolean

Validate that a string is a valid IANA timezone identifier.

f
lazyRef<T>(fn: () => T): [() => T, (value: T | null) => void]

Resettable lazy reference - like once() but can be reset for testing. Returns [get, set] tuple where set(null) resets to uncomputed state.

f
listFiles(prefix: string): Promise<string[]>

List files in storage matching a prefix

f
loadEffectiveDomain(requestUrl: string): string

Load the effective domain from DB, falling back to the request URL hostname.

f
localToUtc(
naive: string,
tz: string
): string

Convert a naive datetime-local value (YYYY-MM-DDTHH:MM) to a UTC ISO string, interpreting the value as local time in the given timezone.

f
logAndNotifyRegistration(entries: EmailEntry[]): Promise<void>

Log attendee registration and send consolidated webhook Used for single-event registrations

f
logDebug(
category: LogCategory,
message: string
): void

Log a debug message with category prefix For detailed debugging during development

f
logError(context: ErrorContext): void

Log a classified error to console.error and persist to the activity log. Console output uses error codes and safe metadata (never PII). Activity log entry is encrypted and visible to admins on the log pages.

f
logErrorLocal(context: ErrorContext): void

Log a classified error to console.error only (no ntfy, no activity log). Use this where calling logError would cause infinite recursion (e.g. ntfy.ts).

f
logRequest(entry: RequestLogEntry): void

Log a completed request to console.debug Path is automatically redacted for privacy

f
mapParallel<T, U>(fn: (item: T) => Promise<U>)

Map over a promise-returning function in parallel (Promise.all)

f
markRefunded(
attendeeId: number,
eventId: number
): Promise<void>

Mark an attendee as refunded for a specific event. Keeps payment_id intact so payment details can still be viewed.

f
mergeEventFields(fieldSettings: EventFields[]): EventFields

Determine which contact fields to collect for multiple events. Returns the union of all field settings, sorted by canonical CONTACT_FIELDS order.

f
normalizeBusinessEmail(email: string): string

Normalizes email: trim and lowercase

f
normalizeDatetime(
value: string,
label: string
): string

Normalize datetime-local "YYYY-MM-DDTHH:MM" to full UTC ISO string. The input is interpreted as local time in the given timezone and converted to UTC.

f
normalizePhone(
phone: string,
prefix: string
): string

Strip non-numeric characters from a phone number and normalize to +{prefix}{local}

f
normalizeSlug(input: string): string

Normalize a user-provided slug: trim, lowercase, replace spaces with hyphens

f
now(): Date

Current time as a Date

f
nowIso(): string

Full ISO-8601 timestamp for created/logged_at fields

f
nowMs(): number

Epoch milliseconds for numeric comparisons

f
once<T>(fn: () => T): (() => T)

Lazy evaluation - compute once on first call, cache forever. Use instead of let x = null; const getX = () => x ??= compute();

f
padAuthToken(serial: string): string

Pad a serial number to meet Apple's minimum authenticationToken length. Uses "-" (not in uppercase hex charset) so padding is cleanly reversible.

f
parseEmbedHosts(input: string): string[]

Parse a comma-separated list of hosts into trimmed, lowercased entries. Filters out empty strings from trailing commas etc.

f
parseEncryptedPayload(
encrypted: string,
prefix: string,
label: string
): { iv: Uint8Array; ciphertext: Uint8Array; }

Parse a prefixed encrypted payload into IV and ciphertext bytes. Validates the prefix and separator; throws on invalid format.

f
parseEventFields(fields: EventFields): ContactField[]

Parse a comma-separated fields string into individual ContactField names

f
parseFlashValue(value: string): { success?: string; error?: string; result?: string; }

Parse a flash cookie value into type, message, and optional result

f
parsePiiBlob(json: string): PiiBlob

Parse a PII blob JSON back into contact fields (defaults v to 1 for pre-versioned blobs)

f
pipe(...fns: Array<(arg: unknown) => unknown>): (value: unknown) => unknown
2 overloads

Compose functions left-to-right (pipe) Uses recursive conditional types for arbitrary-length type safety.

f
queryAll<T>(
sql: string,
args?: InValue[]
): Promise<T[]>

Query all rows, returning a typed array

f
queryAndMap<Row, Out>(toOut: (row: Row) => Promise<Out>)

Execute a SQL query and map result rows through an async transformer.

f
queryOne<T>(
sql: string,
args: InValue[]
): Promise<T | null>

Query single row, returning null if not found

f
randomChoice<T>(arr: readonly T[]): T

Pick a random element from an array

f
rawSql(expr: string): RawSql

Embed a raw SQL expression (e.g. last_insert_rowid())

f
recordFailedLogin(ip: string): Promise<boolean>

Record a failed login attempt Returns true if the account is now locked

f
redactPath(path: string): string

Redact dynamic segments from paths for privacy-safe logging Replaces:

f
registerBunnySubdomain(subdomain: string)

Register a bunny subdomain (DNS + CDN).

f
registerCache(provider: CacheStatProvider): void

Register a cache stat provider (called at module load time)

f
renderEmailContent(
type: EmailTemplateType,
data: TemplateData
): Promise<EmailContent>

Render all 3 parts (subject, html, text) using custom templates with fallback to defaults

f
renderMarkdown(text: string): string

Render markdown to HTML (block-level: paragraphs, lists, etc.). Raw HTML is escaped.

f
renderTemplate(
template: string,
data: TemplateData
): Promise<string>

Render a single Liquid template string with the given data

f
requireEnv(key: string): string

Get a required environment variable, throwing if not set. Use this instead of getEnv(key) as string when the variable must exist.

f
reserveSession(sessionId: string): Promise<ReserveSessionResult>

Reserve a payment session for processing (first phase of two-phase lock) Inserts with NULL attendee_id to claim the session. Returns { reserved: true } if we claimed it, or { reserved: false, existing } if already claimed.

f
resetCacheRegistry(): void

Reset the registry (for testing)

f
resetDatabase(): Promise<void>

Reset the database by dropping all tables (reverse order for FK safety)

f
resetDemoMode(): void

Reset cached demo mode value (for testing and cache invalidation)

f
resetEffectiveDomain(): void

Reset effective domain cache (for testing).

f
resetEngine(): void

For testing: reset the engine (so filters can be re-registered after currency changes)

f
resetGroupEvents(groupId: number): Promise<void>

Reset group assignment on all events in a group.

f
resetHostEmailConfig(): void

For testing: reset host email config to read from env vars.

f
resetSessionCache(): void

Clear session cache (exported for testing)

f
resultRows<T>(result: ResultSet): T[]

Cast libsql ResultSet rows to a typed array (single centralized assertion)

f
runWithPendingWork<T>(fn: () => T): T

Run a function within a pending-work scope

f
runWithRequestId<T>(fn: () => T): T

Run a function with a request-scoped random ID for log correlation

f
runWithSessionContext<T>(fn: () => T): T

Run a function within a session-memoization scope

f
runWithStorageConfig<T>(
config: StorageConfig,
fn: () => T
): T

Run fn with an isolated storage configuration (test-only).

f
safeAsync<T>(
fn: () => Promise<T>,
errorCode: ErrorCodeType
): Promise<T | null>

Safely execute async operation, returning null on error. Re-throws PaymentUserError so user-facing messages propagate.

f
secureCompare(
a: string,
b: string
): boolean

Constant-time string comparison to prevent timing attacks

f
sendEmail(
config: EmailConfig,
msg: EmailMessage
): Promise<number | undefined>

Send a single email via the configured provider. Logs errors, never throws. Returns HTTP status or undefined on non-HTTP errors.

f
sendNtfyError(code: string): Promise<void>

Send an error notification to the configured ntfy URL Returns a promise so callers can await delivery if needed. Delivery failures are logged locally (via logErrorLocal) but never throw.

f
sendRegistrationEmails(
entries: EmailEntry[],
currency: string
): Promise<void>

Send registration confirmation + admin notification emails. Entries is an array because one registration can cover multiple events. Silently skips if email is not configured. Attaches one SVG ticket per entry to the confirmation email.

f
sendRegistrationWebhooks(
entries: RegistrationEntry[],
currency: string
): Promise<void>

Send consolidated webhook to all unique webhook URLs for the given entries

f
sendTestEmail(
config: EmailConfig,
to: string
): Promise<number | undefined>

Send a test email to the business email address. Returns HTTP status or undefined on non-HTTP errors.

f
sendWebhook(
webhookUrl: string,
payload: WebhookPayload,
eventId?: number
): Promise<void>

Send a webhook payload to a URL Fires and forgets - errors are logged but don't block registration

f
setCachedSession(session: AuthSession | null): void

Store the resolved session in the current request scope

f
setDb(client: Client | null): void

Set database client (for testing)

f
setDemoModeForTest(enabled: boolean): void

Explicitly set demo mode on or off (for testing). Bypasses Deno.env to avoid races between parallel test workers.

f
setEffectiveDomainForTest(domain: string): void

Set effective domain directly (for testing).

f
setEncryptionKeyForTest(key: string | null): void

Explicitly set or clear the encryption key for testing. Bypasses Deno.env to avoid races between parallel test workers. Automatically clears all crypto caches (encryption, HMAC, and any registered via onEncryptionKeyChange).

f
setFastPbkdf2ForTest(fast: boolean | null): void

Explicitly enable/disable fast PBKDF2 for testing without env var races

f
setGroupEventsActive(
groupId: number,
active: boolean
): Promise<number>

Set the active flag on every event in a group. Returns the number of events affected.

f
setHostEmailConfigForTest(config: EmailConfig | null): void

For testing: set host email config directly. Bypasses env vars to avoid races.

f
setRsaKeySizeForTest(size: number | null): void

Explicitly set RSA key size for testing without env var races

f
setSuppressDebugLogs(value: boolean | null): void

Set module-level debug log suppression (avoids env race in parallel tests).

f
setSuppressRequestLogs(value: boolean | null): void

Set module-level request log suppression (avoids env race in parallel tests).

f
sha1Hex(data: Uint8Array): string

Compute SHA-1 hex digest of a Uint8Array

f
signCsrfToken(): Promise<string>

Create a signed CSRF token: s1.{timestamp}.{nonce}.{hmac}

f
singleEventAnswerIds(
eventId: number,
answerIds?: number[]
): Record<string, number[]> | undefined

Convert single-event answerIds to the per-event format used in metadata

f
sort<T>(comparator: (
a: T,
b: T
) => number
)

Non-mutating sort with comparator

f
sortEvents<T extends Event>(
events: T[],
holidays: Holiday[]
): T[]

Sort events in unified 3-tier order. Works with any Event subtype (Event, EventWithCount, etc.).

f
startOfHour(date: Date): Date

Round a date down to the start of the current hour for cache-stable signatures

f
symmetricDecrypt(
encrypted: string,
key: CryptoKey
): Promise<string>

Decrypt a prefixed AES-GCM payload with the given key.

f
symmetricEncrypt(
plaintext: string,
key: CryptoKey
): Promise<string>

Encrypt plaintext with an AES-GCM key, returning prefixed format: enc:1:$base64iv:$base64ciphertext

f
toBase64(bytes: Uint8Array): string

Convert Uint8Array to base64 string

f
toBookingItems(items: CheckoutIntent["items"]): BookingItem[]

Convert registration line items to compact booking items

f
toCamelCase(s: string): string

Convert snake_case to camelCase

f
toCheckoutResult(
sessionId: string | undefined,
url: string | undefined | null,
label: LogCategory
): CheckoutSessionResult

Convert a provider-specific checkout result to a CheckoutSessionResult. Returns null if session ID or URL is missing.

f
todayInTz(tz: string): string

Get today's date as YYYY-MM-DD in the given timezone.

f
toMajorUnits(minorUnits: number): string

Convert minor units to major units string for form display. e.g. toMajorUnits(1050) → "10.50" (for GBP)

f
toMinorUnits(majorUnits: number): number

Convert major units (decimal) to minor units (integer). e.g. toMinorUnits(10.50) → 1050 (for GBP)

f
toSnakeCase(s: string): string

Convert camelCase to snake_case

f
trackQuery<T>(
sql: string,
fn: () => Promise<T>
): Promise<T>

Run an async DB operation and log it when tracking is active

f
trimAuthToken(authToken: string): string

Strip padding added by padAuthToken to recover the original serial number

f
tryDeleteFile(
filename: string,
eventId: number | undefined,
detail: string
): Promise<void>

Try to delete a file from storage, logging errors on failure

f
ttlCache<K, V>(
ttlMs: number,
now?: () => number
): TtlCache<K, V>

Create a TTL (Time-To-Live) cache. Entries expire after ttlMs milliseconds. Accepts an optional clock function for testing.

f
unique<T>(array: T[]): T[]

Remove duplicate values (by reference/value equality)

f
uniqueBy<T>(fn: (item: T) => unknown)

Remove duplicates by a key function

f
unlinkAttendeeFromEvent(
attendeeId: number,
eventId: number
): Promise<{ attendeeDeleted: boolean; }>

Remove a single event link for an attendee. If the attendee has no remaining event links, deletes the attendee entirely. Returns whether the attendee was fully deleted.

f
unwrapKey(
wrapped: string,
unwrappingKey: CryptoKey
): Promise<CryptoKey>

Unwrap a symmetric key Expects format: wk:1:$base64iv:$base64wrapped

f
updateAttendeePII(
attendeeId: number,
input: UpdateAttendeePIIInput
): Promise<void>

Update an attendee's PII (name, email, phone, etc.) — shared across all event links. Caller must be authenticated admin (public key always exists after setup).

f
updateBusinessEmail(email: string): Promise<void>

Updates the business email in the database. Pass empty string to clear the business email. Email is encrypted at rest.

f
updateCheckedIn(
attendeeId: number,
eventId: number,
checkedIn: boolean
): Promise<void>

Update an attendee's checked_in status for a specific event. Caller must be authenticated admin (public key always exists after setup)

f
uploadAttachment(
data: Uint8Array,
filename: string
): Promise<string>

Upload an attachment to Bunny storage. Encrypts the file bytes before uploading. Uses the provided filename (caller generates via generateAttachmentFilename). Returns the filename on success.

f
uploadImage(
data: Uint8Array,
detectedType: string
): Promise<string>

Upload an image to Bunny storage. Encrypts the image bytes before uploading. Returns the filename (without path) on success.

f
uploadRaw(
data: Uint8Array,
filename: string
): Promise<string>

Upload raw bytes to storage, routing to local or Bunny based on config

f
utcToLocalInput(
utcIso: string,
tz: string
): string

Convert a UTC ISO datetime string to a datetime-local input value (YYYY-MM-DDTHH:MM) in the given timezone. Used for pre-populating form inputs with timezone-adjusted values.

f
validateAttachment(data: Uint8Array): AttachmentValidationResult

Validate an attachment file: check size only (any file type allowed).

f
validateCustomDomain(hostname: string): Promise<BunnyApiResult>

Validate a custom domain (delegates to bunnyCdnApi for testability).

f
validateEmbedHosts(input: string): string | null

Validate a comma-separated list of host patterns. Returns null if all valid, or the first error message.

f
validateEncryptionKey(): void

Validate encryption key is present and valid Call this on startup to fail fast if key is missing

f
validateGroupEventType(
groupId: number,
eventType: EventType,
excludeEventId?: number
): Promise<string | null>

Validate that an event type is compatible with a group's existing events. Returns an error message if mismatched, null if OK. Pass excludeEventId to skip a specific event (for edit-self case).

f
validateHostPattern(host: string): string | null

Validate a single host pattern Returns null if valid, or an error message if invalid

f
validateImage(
data: Uint8Array,
contentType: string
): ImageValidationResult

Validate an image file: check MIME type, size, and magic bytes.

f
validatePrice(
raw: string,
minPrice: number,
maxPrice: number
): PriceResult

Validate and convert a raw price string to minor units. Returns ok with 0 if raw is empty and minPrice is 0 (pay-what-you-want with no input). Returns error if raw is empty and minPrice > 0, or if parsed value is out of range.

f
validateSlug(slug: string): string | null

Validate a normalized slug. Returns error message or null.

f
validateTemplate(template: string): string | null

Validate a Liquid template by parsing it (no rendering). Returns null if valid, or an error message string if invalid.

f
verifyPassword(
password: string,
storedHash: string
): Promise<boolean>

Verify a password against a hash Uses constant-time comparison to prevent timing attacks

f
verifySignedCsrfToken(
token: string,
maxAge?
): Promise<boolean>

Verify a signed CSRF token's signature and expiry

f
verifyUserPassword(
user: User,
password: string
): Promise<string | null>

Verify a user's password (decrypt stored hash, then verify) Returns the decrypted password hash if valid (needed for KEK derivation)

f
withCacheInvalidation<Row, Input>(
table: Table<Row, Input>,
invalidate: () => void
): Table<Row, Input>

Wrap a table so that insert, update, and deleteById automatically call an invalidation callback (e.g. cache invalidation). Eliminates the repeated spread-and-override pattern in groups/holidays/events.

f
withCheckoutError(op: () => Promise<CheckoutSessionResult>): Promise<CheckoutSessionResult>

Wrap a checkout operation, converting PaymentUserError to { error } result and swallowing unexpected errors as null. Used by both provider adapters.

f
withRequiredEmail(fields: EventFields): EventFields

Ensure "email" is included in an event fields setting

f
wrapKey(
keyToWrap: CryptoKey,
wrappingKey: CryptoKey
): Promise<string>

Wrap a symmetric key with another key using AES-GCM Returns format: wk:1:$base64iv:$base64wrapped

f
wrapKeyWithToken(
keyToWrap: CryptoKey,
sessionToken: string
): Promise<string>

Wrap a key using a session token (derives a wrapping key from the token)

f
writeClosesAt(v: string | null): Promise<string | null>

Encrypt closes_at for DB storage (null/empty → encrypted empty)

f
writeEventDate(v: string): Promise<string>

Encrypt event date for DB storage

Interfaces

I
PaymentProvider

Payment provider interface.

I
Table

Table definition with CRUD operations

Type Aliases

T
AdminEvent = Omit<EventWithCount, "slug_index">

Admin API event shape — all event fields except internal indices. Used by both admin JSON API and admin templates to ensure consistent field exposure. Snake_case keys match the DB schema.

T
AttachmentValidationError = "too_large"

Attachment validation error

T
AttendeeInput =
ContactFields
& { paymentId?: string; bookings: EventBooking[]; }

Input for creating an attendee atomically (one or more events)

T
T
BookingItem = { e: number; q: number; p: number; }

Compact booking item stored in session metadata (serialized/deserialized as JSON)

T
ColumnDef<T = unknown> = { generated?: boolean; default?: () => T; write?: (v: T) => Promise<T> | T; read?: (v: T) => Promise<T> | T; }

Column definition for a table

T
ContactFields =
Pick<ContactInfo, "name" | "email">
& Partial<
Pick<ContactInfo, "phone" | "address" | "special_instructions">
>

Required name+email with optional phone/address/special_instructions from ContactInfo

T
DemoFieldMap = Record<string, readonly string[]>

Maps form field names to arrays of possible demo values

T
EmailEvent =
WebhookEvent
& { date: string; location: string; purchase_only: boolean; assign_built_site: boolean; }

Event data needed for registration pipeline (extends webhook event with display + assignment fields)

T
EmailProvider = keyof PROVIDERS

Union of all supported email provider keys, derived from the PROVIDERS map

T
ErrorContext = { code: ErrorCodeType; eventId?: number; attendeeId?: number; detail?: string; }

Error log context (privacy-safe metadata only)

T
EventFields = string

Contact fields setting for an event (comma-separated ContactField names, or empty for name-only). Alias kept for documentation; runtime enforcement happens in parseEventFields.

T
EventType = "standard" | "daily"

Event type: standard (one-time) or daily (date-based booking)

T
PassData = { serialNumber: string; organizationName: string; description: string; eventName: string; eventDate: string; eventLocation: string; attendeeDate: string | null; quantity: number; pricePaid: number; currencyCode: string; checkinUrl: string; webServiceURL: string; foregroundColor?: string; backgroundColor?: string; labelColor?: string; }

Data needed to generate a pass — maps to existing ticket/event data

T
PaymentProviderType = "stripe" | "square"

Supported payment provider identifiers

T
TableSchema<Row> = [K in keyof Row]: ColumnDef<Row[K]>

Table schema definition Keys are DB column names (snake_case), values are column definitions

Variables

v
activityLogTable

Activity log table definition message is encrypted - decrypted only for admin view

v
API_AVAILABILITY_EXAMPLE_JSON: string

Example availability response JSON

v
API_BOOK_FREE_EXAMPLE_JSON: string

Example free booking response JSON

v
API_BOOK_PAID_EXAMPLE_JSON: string

Example paid booking response JSON

v
API_BOOK_REQUEST_JSON: string

Example booking request body

v
API_EXAMPLE_EVENT: EventWithCount

Example event matching the webhook example data

v
API_EXAMPLE_PUBLIC_EVENT: PublicEvent

The example PublicEvent, produced by toPublicEvent

v
API_LIST_EXAMPLE_JSON: string

Example list response JSON

v
API_SINGLE_EXAMPLE_JSON: string

Example single-event response JSON

v
ATTACHMENT_ERROR_MESSAGES: Record<AttachmentValidationError, string>

User-facing messages for attachment validation errors

v
ATTENDEE_JOIN_SELECT: string

SELECT clause for attendee + event_attendees JOINs (INNER JOIN context). Derives date from start_at for backward compatibility with the Attendee type.

v
ATTENDEE_LEFT_JOIN_SELECT: string

SELECT clause for LEFT JOIN context — COALESCEs nullable join columns so attendees with broken/missing event_attendees linkage still appear in results (with event_id=0 as an obvious corruption indicator).

v
col: { boolean: (defaultValue: boolean) => ColumnDef<boolean>; converted: <App>(config: { default?: () => App; write: (v: App) => InValue; read: (raw: InValue) => App; }) => ColumnDef<App>; encrypted: <T>(
encrypt: ColumnTransform<T>,
decrypt: ColumnTransform<T>
) => ColumnDef<T>
; encryptedNullable: <T>(def: ColumnDef<T>) => ColumnDef<T | null>; encryptedText: (
encrypt: ColumnTransform<string>,
decrypt: ColumnTransform<string>
) => ColumnDef<string>
; generated: <T>() => ColumnDef<T>; simple: <T>() => ColumnDef<T>; transform: <T>(
write: (v: T) => Promise<T> | T,
read: (v: T) => Promise<T> | T
) => ColumnDef<T>
; withDefault: <T>(defaultFn: () => T) => ColumnDef<T>; }

Helper to create column definitions

v
CONFIG_KEYS: { APPLE_WALLET_PASS_TYPE_ID: string; APPLE_WALLET_SIGNING_CERT: string; APPLE_WALLET_SIGNING_KEY: string; APPLE_WALLET_TEAM_ID: string; APPLE_WALLET_WWDR_CERT: string; ATTENDEE_COLUMN_ORDER: string; BOOKING_FEE: string; BUNNY_SUBDOMAIN: string; BUSINESS_EMAIL: string; CONTACT_PAGE_TEXT: string; COUNTRY: string; CURRENT_TASK: string; CUSTOM_DOMAIN: string; CUSTOM_DOMAIN_LAST_VALIDATED: string; EMAIL_API_KEY: string; EMAIL_FROM_ADDRESS: string; EMAIL_PROVIDER: string; EMAIL_TPL_ADMIN_HTML: string; EMAIL_TPL_ADMIN_SUBJECT: string; EMAIL_TPL_ADMIN_TEXT: string; EMAIL_TPL_CONFIRMATION_HTML: string; EMAIL_TPL_CONFIRMATION_SUBJECT: string; EMAIL_TPL_CONFIRMATION_TEXT: string; EMBED_HOSTS: string; EVENT_COLUMN_ORDER: string; GOOGLE_WALLET_ISSUER_ID: string; GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL: string; GOOGLE_WALLET_SERVICE_ACCOUNT_KEY: string; HEADER_IMAGE_URL: string; HOMEPAGE_TEXT: string; LAST_PRUNED_LOGINS: string; LAST_PRUNED_PAYMENTS: string; LAST_PRUNED_SESSIONS: string; LAST_PRUNED_TOKENS: string; LATEST_SCRIPT_VERSION: string; LATEST_SCRIPT_VERSION_NAME: string; PAYMENT_PROVIDER: string; PUBLIC_KEY: string; SETUP_COMPLETE: string; SHOW_PUBLIC_API: string; SHOW_PUBLIC_SITE: string; SQUARE_ACCESS_TOKEN: string; SQUARE_LOCATION_ID: string; SQUARE_SANDBOX: string; SQUARE_WEBHOOK_SIGNATURE_KEY: string; STRIPE_SECRET_KEY: string; STRIPE_WEBHOOK_ENDPOINT_ID: string; STRIPE_WEBHOOK_SECRET: string; TERMS_AND_CONDITIONS: string; THEME: string; WEBSITE_TITLE: string; WRAPPED_PRIVATE_KEY: string; }
v
CONTACT_FIELDS: readonly ContactField[]

All valid contact field names (runtime array matching the ContactField union)

v
CSRF_INVALID_FORM_MESSAGE: "Invalid or expired form. Please try again."

Default message for invalid/expired CSRF form submissions

v
DEFAULT_BOOKABLE_DAYS: string[]

Default bookable days (all days of the week)

v
DEFAULT_TIMEZONE: "Europe/London"

Default timezone when none is configured

v
DEMO_EVENT_DESCRIPTIONS: readonly string[]

Demo event descriptions — rock-themed gig blurbs assembled from word pools. Like the names above, the list is procedurally generated but seeded so it stays deterministic across runs.

v
DEMO_EVENT_LOCATIONS: readonly string[]

Demo event locations — pretend rock-venue / festival listings. Procedurally generated from the venue word pools using a seeded PRNG.

v
DEMO_EVENT_NAMES: readonly string[]

Demo event names — pretend rock/heavy-metal band listings. Generated procedurally from a seeded PRNG so the list stays deterministic across runs (tests rely on this) but offers far more variety than a hand-curated list while staying on-theme.

v
DOMAIN_PATTERN: RegExp

Matches a valid hostname like "example.com" or "sub.example.com"

v
EMAIL_PROVIDER_LABELS: Record<EmailProvider, string>

Display labels for email providers — keys must match EmailProvider

v
ErrorCode: [K in keyof ErrorDefs]: ErrorDefs[K][0]

Error code strings for use in logError calls

v
errorCodeLabel: Record<ErrorCodeType, string>

Human-readable labels for error codes (shown in admin activity log)

v
EVENT_DEMO_FIELDS: DemoFieldMap

Event metadata fields

v
executeBatchWithResults

Execute multiple write statements and return their ResultSets. Statements run in order within a single transaction (Turso batch API). Ideal for cascading deletes and multi-step writes.

v
GROUP_DEMO_FIELDS: DemoFieldMap

Group name and description fields

v
groupsTable

Groups table with CRUD operations — writes auto-invalidate the cache

v
holidaysTable

Holidays table with CRUD operations — writes auto-invalidate the cache

v
IMAGE_ERROR_MESSAGES: Record<ImageValidationError, string>

User-facing messages for image validation errors

v
isAdminLevel

Type guard: check if a string is a valid AdminLevel

v
isContactField

Type guard: check if an arbitrary string is a valid ContactField

v
isEventType

Type guard: check if an arbitrary string is a valid EventType

v
isPaymentProvider

Type guard: check if a string is a valid PaymentProviderType

v
joinStrings

Join an array of strings into a single string (curried reduce shorthand). Replaces the common pattern: reduce((acc: string, s: string) => acc + s, "")

v
MAX_ATTACHMENT_SIZE

Maximum attachment file size in bytes (default: 25MB)

v
PII_BLOB_VERSION: 1

Current PII blob schema version

v
queryBatch

Execute multiple read queries in a single round-trip using Turso batch API.

v
SCHEMA_TABLE_NAMES: string[]

Ordered table names — matches FK dependency order (parents before children)

v
SEED_MAX_ATTENDEES: 100000

Max attendees per seeded event

v
settings: { appleWallet; email: { get apiKey(): string; get fromAddress(): string; get hasApiKey(): boolean; get provider(): string; template(
type: EmailTemplateType,
format: EmailTemplateFormat
): string; templateSet(type: EmailTemplateType): { subject: string; html: string; text: string; }; }
; getCachedRaw; googleWallet; invalidateCache; loadAll; setRaw; setup: { clearCache; complete; isComplete; }; square: { get accessToken(): string; get hasToken(): boolean; get locationId(): string; get sandbox(): boolean; get webhookSignatureKey(): string; }; stripe: { get hasKey(): boolean; get keyMode(): "test" | "live" | null; get secretKey(): string; get webhookEndpointId(): string; get webhookSecret(): string; }; update: { appleWallet; attendeeColumnOrder; bookingFee: (v: string) => Promise<void>; bunnySubdomain; businessEmail; clearPaymentProvider: () => Promise<void>; contactPageText; country: (v: string) => Promise<void>; currentTask; customDomain; customDomainLastValidated: () => Promise<void>; email: { apiKey; fromAddress; provider; template: (
type: EmailTemplateType,
format: EmailTemplateFormat,
content: string
) => Promise<void>
; }
; embedHosts; eventColumnOrder; googleWallet; headerImageUrl; homepageText; lastPrunedLogins; lastPrunedPayments; lastPrunedSessions; lastPrunedTokens; latestScriptVersion; latestScriptVersionName; paymentProvider: (v: PaymentProviderType) => Promise<void>; showPublicApi; showPublicSite; square: { accessToken; locationId; sandbox; webhookSignatureKey; }; stripe: { secretKey; webhookConfig: (config: { secret: string; endpointId: string; }) => Promise<void>; }; terms; theme: (v: Theme) => Promise<void>; websiteTitle; }
; updateUserPassword; withCurrentTask; get attendeeColumnOrder(): string; get bookingFee(): string; get bunnySubdomain(): string; get businessEmail(): string; clearTestOverride(...keys: (keyof SettingsData)[]): void; clearTestOverrides(): void; get contactPageText(): string; get country(): string; get currency(): string; get currentTask(): string; get customDomain(): string; get customDomainLastValidated(): string; get embedHosts(): string; get eventColumnOrder(): string; get headerImageUrl(): string; get homepageText(): string; get lastPrunedLogins(): string; get lastPrunedPayments(): string; get lastPrunedSessions(): string; get lastPrunedTokens(): string; get latestScriptVersion(): string; get latestScriptVersionName(): string; get paymentProvider(): PaymentProviderType | null; get phonePrefix(): string; get publicKey(): string; setForTest(overrides: Partial<SettingsData>): void; get showPublicApi(): boolean; get showPublicSite(): boolean; get terms(): string; get theme(): Theme; get timezone(): string; get websiteTitle(): string; get wrappedPrivateKey(): string; }
v
v
SQUARE_METADATA_MAX_VALUE_LENGTH: 255

Square metadata constraint: each value max 255 characters

v
STALE_RESERVATION_MS

Threshold for abandoned payment reservations in ms (default: 300000 = 5 min)

v
STRIPE_METADATA_MAX_VALUE_LENGTH: 500

Stripe metadata constraint: each value max 500 characters

v
TERMS_DEMO_FIELDS: DemoFieldMap

Terms and conditions field

v
VALID_EMAIL_PROVIDERS: ReadonlySet<EmailProvider>

Valid provider names, derived from the PROVIDERS map

v
WALLET_ICONS: Record<string, Uint8Array>

Decoded icon files for inclusion in .pkpass bundles

v
WEBHOOK_EXAMPLE_JSON: string

Pretty-printed JSON for embedding in documentation

v
WEBHOOK_EXAMPLE_PAYLOAD: WebhookPayload

The example payload, matching what buildWebhookPayload would produce

Usage

import * as mod from "doc.ts";