Chobble Tickets
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.
Activate a user by wrapping the data key with their KEK
Add days to a YYYY-MM-DD date string
Add a new event link for an existing attendee with atomic capacity check. Does NOT create a new attendee or touch PII — just inserts an event_attendees row.
Queue a promise that must complete before the response is sent
Record a query (no-op when logging is disabled)
AES-GCM decrypt raw data, returning the decrypted ArrayBuffer
AES-GCM encrypt raw data, returning IV and ciphertext bytes
Append iframe=true query param to a URL when in iframe mode
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.
Assign events to a group by updating their group_id.
Narrow an unknown value to string, defaulting to "" if not a string.
Replaces typeof x === "string" ? x : "" at type boundaries.
Convert standard base64 to base64url (no padding). Works on both strings and Uint8Array (bytes are first encoded to base64).
Resource management pattern (like Haskell's bracket or try-with-resources). Ensures cleanup happens even if the operation throws.
Build an INSERT statement for the attendees table from encrypted fields.
Build a capacity-checked INSERT into event_attendees.
Build the check-in URL for a single ticket token
Build embed snippets (script and iframe variants) for a ticket URL
Build a frame-ancestors CSP value from allowed embed hosts. Returns null if the list is empty (allow embedding from anywhere).
Build info lines from ticket data (non-PII event and booking details)
Build input key mapping from DB columns snake_case DB column → camelCase input key
Build checkout metadata from a CheckoutIntent (converts items to compact form).
Build checkout session metadata from booking data (items already compact).
Build a PII blob JSON from contact fields
Build a complete .pkpass file as a Uint8Array (ZIP archive)
Build the full subdomain record name (user choice + suffix). e.g. "myevent" + ".tickets" → "myevent.tickets"
Build SVG ticket data from an email entry (non-PII only)
Build the data object exposed to Liquid templates
Generate SVG ticket attachments for all entries
Build a consolidated webhook payload from registration entries
Wrapper for test mocking - delegates to attendeesApi at runtime
Check a capacity-guarded write result and invalidate cache on success
Split an array into chunks of a given size
Clear login attempts for an IP (on successful login)
Clear stored ticket tokens for a session (after redirect has consumed them)
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.
Remove null and undefined values from array
Compute slug index from slug for blind index lookup
Compute HMAC-SHA256 using Web Crypto API, returning raw ArrayBuffer
Compute slug index from slug for blind index lookup
Compute ticket token index using HMAC for blind lookups Similar to slug_index for events - allows lookup without decrypting
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.
Extract ContactInfo fields from an object
Wrapper for test mocking - delegates to attendeesApi at runtime
Create an invited user (no password yet, has invite code)
Create manifest.json mapping filenames to SHA-1 hashes
Create a request timer for measuring duration
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.
Create a new session with CSRF token, wrapped data key, and user ID Token is hashed before storage for security
Create a type guard from a readonly array of string literal values
Create a new user with encrypted fields
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.
Convert nullable date to start_at/end_at (null-safe wrapper around dateToRange)
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).
Decrypt a string value encrypted with encrypt() Expects format: enc:1:$base64iv:$base64ciphertext
Decrypt a user's admin level
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.
Decrypt a single raw attendee, handling null input. Used when attendee is fetched via batch query.
Decrypt attendee PII using the private key Used in admin views after obtaining private key from session
Decrypt a list of raw attendees (all fields). Used when attendees are fetched via batch query.
Decrypt binary data encrypted with encryptBytes(). Expects ENCB binary format: magic + version + IV + ciphertext.
Decrypt the ticket_tokens field from a processed payment record. Returns the plaintext token string (e.g. "tok1+tok2") or empty string.
Decrypt a user's username
Decrypt data with a symmetric key
Helper for tables whose primary key column is id.
Define a table with CRUD operations
Delete all storage files (images and attachments) for a list of events
Delete all sessions (used when password is changed)
Delete all stale reservations (unfinalized and older than STALE_RESERVATION_MS). Called from admin event views to clean up abandoned checkouts.
Delete an attendee and all its event links, payments, and answers.
Delete rows matching a field value
Delete rows from multiple tables in a single batch transaction
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.
Delete the image and attachment files for a single event
Delete a file, routing to local or Bunny based on config.
Delete all sessions except the current one Token is hashed before database comparison
Delete a session by token Token is hashed before database lookup
Delete a stale reservation to allow retry
Delete a user and all their sessions and API keys
Upload and publish new script code to Bunny CDN.
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
Detect iframe mode from a request URL and store it for the current request
Detect the actual image type from magic bytes. Returns the MIME type if matched, null otherwise.
Download and decrypt a file. Returns the decrypted bytes, or null if the file does not exist.
Download raw bytes from storage. Returns null if the file does not exist.
Enable query logging and clear previous entries
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)
Encrypt attendee fields into a PII blob, returning null if key not configured
Encrypt attendee PII using the public key from settings This can be called without authentication (for public ticket forms)
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).
Shared encrypted name column for tables that store a display name.
Encrypt a PII blob JSON string with the public key
Encrypt data with a symmetric key (for wrapping private key with DATA_KEY)
Enforce metadata value length limits for a payment provider.
Extract a human-readable message from an unknown caught value
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.
Whether this event can send QR code scanners directly to checkout. True when no extra contact fields or questions are required.
Execute multiple write statements, discarding results.
Normalize validated session metadata into the canonical SessionMetadata shape.
Extract the inner content of an SVG element (strip the outer <svg> wrapper)
Extract the viewBox from an SVG element to compute its coordinate space
Curried filter
Finalize a reserved session with the created attendee ID (second phase)
Curried flatMap
Await all queued work. Call before returning the response.
Format an amount in minor units (pence/cents) as a currency string. e.g. formatCurrency(1050) → "£10.50" (when currency is GBP)
Format a YYYY-MM-DD date for display. Returns "Monday 15 March 2026"
Format a UTC ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
Format an ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
Compact ISO datetime formatter for table cells. Returns e.g. "07/04/2026 14:00" in the configured timezone.
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.
Format an error context into a human-readable activity log message
Format IV + ciphertext as a prefixed base64 string
Format an error detail string with request context and error message
Convert base64 string to Uint8Array
Generate a random CDN filename preserving the original name for readability
Generate a random 256-bit symmetric key for data encryption
Generate a random filename with the correct extension
Generate an RSA key pair for asymmetric encryption Returns { publicKey, privateKey } as exportable JWK strings
Build the pass.json content from pass data and signing credentials
Generate an SVG string for a QR code encoding the given text. Returns a complete <svg> element suitable for inline embedding.
Generate a cryptographically secure random token Uses Web Crypto API getRandomValues
Generate a random slug with at least 2 digits and 2 letters. Uses Fisher-Yates shuffle on the fixed positions to avoid bias.
Generate a standalone SVG ticket with QR code and event/booking details. Returns a complete SVG document string.
Generate a 5-byte uppercase hex ticket token for public ticket URLs
Generate a unique slug by retrying until one is not taken.
Get active events in a group with attendee counts.
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.
Get active holidays (end_date >= today) for date computation (from cache). "today" is computed in the configured timezone.
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.
Get all activity log entries (most recent first)
Collect stats from all registered caches
Get all daily events with attendee counts (from cache).
Get all events with attendee counts (from cache)
Get all groups, decrypted, ordered by id (from cache)
Get all holidays, decrypted, ordered by start_date (from cache)
Get all sessions ordered by expiration (newest first)
Get all standard events with attendee counts (from cache). Used by the calendar view to include one-time events on their scheduled date.
Get all users (for admin user management page, from cache)
Get an attendee by ID (decrypted) Requires private key for decryption - only available to authenticated sessions
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)
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.
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.
Get attendees for an event without decrypting PII Used for tests and operations that don't need decrypted data
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.
Get booking fee percentage from database. Returns 0 if not set.
Get the Bunny CDN API key from environment
Get the Bunny DNS subdomain suffix (e.g. ".tickets") from environment
Get the Bunny DNS zone ID from environment
Get the Bunny Edge Script ID from environment
Return the cached session if already resolved, or undefined if not yet resolved
Get CDN hostname (delegates to bunnyCdnApi for testability).
Get the most recently generated CSRF token (for synchronous JSX rendering)
Get distinct attendee dates for daily events. Used for the calendar date picker (lightweight, no attendee data).
Get raw attendees for daily events on a specific date. Bounded query: only returns attendees matching the given date.
Get the total attendee quantity for a specific event + date
Get or create database client
Get the number of decimal places for a currency code
Get the effective domain synchronously (must call loadEffectiveDomain first).
Read email config from DB settings. Falls back to business email for fromAddress. Returns null if not configured.
Get allowed embed hosts from database (encrypted, parsed to array) Returns empty array if not configured (embedding allowed from anywhere)
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
Get an environment variable value Checks process.env first (Bunny Edge), falls back to Deno.env (local dev)
Get a single event by ID (from cache)
Get activity log entries for an event (most recent first)
Get all events in a group with attendee counts (including inactive).
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.
Get event and its activity log in a single database round-trip. Uses batch API to reduce latency for remote databases.
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.
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.
Get event with attendee count (from cache)
Get event with attendee count by slug (from cache)
Get a single group by slug_index (from cache)
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.
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.
Returns undefined when no group cap applies: ungrouped, uncapped
group, or daily event without a date.
Get host-level email config. Uses test override if set, otherwise reads env vars.
Get the current request's iframe mode
Get the proxy URL path for serving a decrypted image. Images are encrypted on CDN, so they must be served through the proxy.
Get the MIME type for an image filename from its extension.
Get the newest attendees across all events without decrypting PII. Used for the admin dashboard to show recent registrations.
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.
Derive the private key from session credentials Used to decrypt attendee PII in admin views Results are cached per session token for 10 seconds
Get the attendee ID for an already-processed session Used to return success for idempotent webhook retries
Return a snapshot of all logged queries
Return the start time recorded by enableQueryLog()
Generate random bytes using Web Crypto API
Get the current request ID, or empty string if outside request context
Get a session by token (with 10s TTL cache) Token is hashed for database lookup
Returns which storage backend is active: "bunny", "local", or "none".
Get ungrouped events (group_id = 0) with attendee counts.
Get a user by ID (from cache)
Find a user by invite code hash Scans all users, decrypts invite_code_hash, and compares
Look up a user by username (using blind index, from cache)
Wrapper for test mocking - delegates to attendeesApi at runtime
Hash an invite code using SHA-256
Hash a password using PBKDF2 Returns format: pbkdf2:iterations:$base64salt:$base64hash
Hash a session token using SHA-256 Used to store session lookups without exposing the actual token
Check if a user has set their password (password_hash is non-empty encrypted value)
True when running inside a runWithPendingWork scope (i.e. a request).
Validate that session metadata contains required fields (name + items).
HMAC-SHA256 hash using DB_ENCRYPTION_KEY Used for blind indexes and hashing limited keyspace values Returns deterministic output for same input (unlike encrypt)
Convert ArrayBuffer to base64 string
Convert ArrayBuffer to hex string
Decrypt data using hybrid encryption Expects format: hyb:1:$base64WrappedKey:$base64iv:$base64ciphertext Results are cached in a bounded LRU (ciphertext -> plaintext)
Encrypt data using hybrid encryption (RSA + AES)
Shared columns for tables with encrypted slug + blind-index slug_index.
Import a CryptoKey from DB_ENCRYPTION_KEY.
Import a private key from JWK string
Import a public key from JWK string
Increment the attachment download counter for an attendee. Uses atomic SQL increment to avoid race conditions.
Initialize database tables — idempotent, safe to call on every startup. Uses an advisory lock to prevent concurrent migrations.
Build SQL placeholders for an IN clause, e.g. "?, ?, ?"
Build an INSERT statement from a table name and column→value record.
Invalidate the events cache (for testing or after writes).
Invalidate the groups cache (for testing or after writes).
Invalidate the holidays cache (for testing or after writes).
Invalidate the users cache (for testing or after writes).
Check if Bunny CDN pull zone management is enabled Requires both BUNNY_API_KEY and BUNNY_SCRIPT_ID to be set
Check if Bunny DNS subdomain feature is enabled. Requires BUNNY_API_KEY and BUNNY_DNS_ZONE_ID to be set.
Check if demo mode is enabled
Type guard: checks if a string is a valid EmailProvider
Check if a group slug is already in use. Checks both events and groups for cross-table uniqueness.
Check if a user's invite has expired. Callers should skip this for users who have already set a password.
Check if a user's invite is still valid (not expired, has invite code)
Check if IP is rate limited for login
Whether an event can accept payments (has a price or allows pay-what-you-want)
Check if payments are enabled (any provider configured with valid keys)
Type guard: check if a string is a valid PaymentStatus
Whether query logging is currently active
Check if the system is in read-only mode (READ_ONLY env var)
Check if a reservation is stale (abandoned by a crashed process)
Check if a payment session has already been processed
Check whether a token uses the signed format
Check if a slug is already in use (optionally excluding a specific event ID) Uses slug_index for lookup (blind index)
Check if image storage is enabled (Bunny CDN or local filesystem).
Check if a username is already taken
Validates a basic email format: something@something.something
Check if a naive datetime-local string is a parseable datetime. Does not interpret timezone — purely a format check.
Validate that a string is a parseable PEM certificate
Validate that a string is a parseable PEM private key
Validate that a string is a valid IANA timezone identifier.
Resettable lazy reference - like once() but can be reset for testing. Returns [get, set] tuple where set(null) resets to uncomputed state.
List files in storage matching a prefix
Load the effective domain from DB, falling back to the request URL hostname.
Load all events with holidays and return them sorted, filtered by predicate.
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.
Log attendee registration and send consolidated webhook Used for single-event registrations
Log a debug message with category prefix For detailed debugging during development
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.
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).
Log a completed request to console.debug Path is automatically redacted for privacy
Curried map
Map over a promise-returning function in parallel (Promise.all)
Mark an attendee as refunded for a specific event. Keeps payment_id intact so payment details can still be viewed.
Determine which contact fields to collect for multiple events. Returns the union of all field settings, sorted by canonical CONTACT_FIELDS order.
Normalizes email: trim and lowercase
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.
Strip non-numeric characters from a phone number and normalize to +{prefix}{local}
Normalize a user-provided slug: trim, lowercase, replace spaces with hyphens
Current time as a Date
Full ISO-8601 timestamp for created/logged_at fields
Epoch milliseconds for numeric comparisons
Lazy evaluation - compute once on first call, cache forever.
Use instead of let x = null; const getX = () => x ??= compute();
Pad a serial number to meet Apple's minimum authenticationToken length. Uses "-" (not in uppercase hex charset) so padding is cleanly reversible.
Parse a comma-separated list of hosts into trimmed, lowercased entries. Filters out empty strings from trailing commas etc.
Parse a prefixed encrypted payload into IV and ciphertext bytes. Validates the prefix and separator; throws on invalid format.
Parse a comma-separated fields string into individual ContactField names
Parse a flash cookie value into type, message, and optional result
Parse a PII blob JSON back into contact fields (defaults v to 1 for pre-versioned blobs)
Compose functions left-to-right (pipe) Uses recursive conditional types for arbitrary-length type safety.
Query all rows, returning a typed array
Execute a SQL query and map result rows through an async transformer.
Query single row, returning null if not found
Pick a random element from an array
Embed a raw SQL expression (e.g. last_insert_rowid())
Record a failed login attempt Returns true if the account is now locked
Redact dynamic segments from paths for privacy-safe logging Replaces:
Curried reduce
Register a bunny subdomain (DNS + CDN).
Register a cache stat provider (called at module load time)
Render all 3 parts (subject, html, text) using custom templates with fallback to defaults
Render markdown to HTML (block-level: paragraphs, lists, etc.). Raw HTML is escaped.
Render a single Liquid template string with the given data
Get a required environment variable, throwing if not set.
Use this instead of getEnv(key) as string when the variable must exist.
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.
Reset the registry (for testing)
Reset the database by dropping all tables (reverse order for FK safety)
Reset cached demo mode value (for testing and cache invalidation)
Reset effective domain cache (for testing).
For testing: reset the engine (so filters can be re-registered after currency changes)
Reset group assignment on all events in a group.
For testing: reset host email config to read from env vars.
Clear session cache (exported for testing)
Cast libsql ResultSet rows to a typed array (single centralized assertion)
Run a function within a pending-work scope
Run a function with a request-scoped random ID for log correlation
Run a function within a session-memoization scope
Run fn with an isolated storage configuration (test-only).
Safely execute async operation, returning null on error. Re-throws PaymentUserError so user-facing messages propagate.
Constant-time string comparison to prevent timing attacks
Send a single email via the configured provider. Logs errors, never throws. Returns HTTP status or undefined on non-HTTP errors.
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.
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.
Send consolidated webhook to all unique webhook URLs for the given entries
Send a test email to the business email address. Returns HTTP status or undefined on non-HTTP errors.
Send a webhook payload to a URL Fires and forgets - errors are logged but don't block registration
Store the resolved session in the current request scope
Set database client (for testing)
Explicitly set demo mode on or off (for testing). Bypasses Deno.env to avoid races between parallel test workers.
Set effective domain directly (for testing).
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).
Explicitly enable/disable fast PBKDF2 for testing without env var races
Set the active flag on every event in a group.
Returns the number of events affected.
For testing: set host email config directly. Bypasses env vars to avoid races.
Explicitly set RSA key size for testing without env var races
Set module-level debug log suppression (avoids env race in parallel tests).
Set module-level request log suppression (avoids env race in parallel tests).
Set a user's password (for invite flow)
Compute SHA-1 hex digest of a Uint8Array
Create a signed CSRF token: s1.{timestamp}.{nonce}.{hmac}
Sign the manifest with PKCS#7 detached signature
Convert single-event answerIds to the per-event format used in metadata
Non-mutating sort with comparator
Sort events in unified 3-tier order. Works with any Event subtype (Event, EventWithCount, etc.).
Round a date down to the start of the current hour for cache-stable signatures
Decrypt a prefixed AES-GCM payload with the given key.
Encrypt plaintext with an AES-GCM key, returning prefixed format: enc:1:$base64iv:$base64ciphertext
Convert Uint8Array to base64 string
Convert registration line items to compact booking items
Convert snake_case to camelCase
Convert a provider-specific checkout result to a CheckoutSessionResult. Returns null if session ID or URL is missing.
Get today's date as YYYY-MM-DD in the given timezone.
Convert minor units to major units string for form display. e.g. toMajorUnits(1050) → "10.50" (for GBP)
Convert major units (decimal) to minor units (integer). e.g. toMinorUnits(10.50) → 1050 (for GBP)
Convert camelCase to snake_case
Run an async DB operation and log it when tracking is active
Strip padding added by padAuthToken to recover the original serial number
Try to delete a file from storage, logging errors on failure
Create a TTL (Time-To-Live) cache. Entries expire after ttlMs milliseconds. Accepts an optional clock function for testing.
Remove duplicate values (by reference/value equality)
Remove duplicates by a key function
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.
Unwrap a symmetric key Expects format: wk:1:$base64iv:$base64wrapped
Unwrap a key using a session token
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).
Updates the business email in the database. Pass empty string to clear the business email. Email is encrypted at rest.
Update an attendee's checked_in status for a specific event. Caller must be authenticated admin (public key always exists after setup)
Update a single event link's quantity and date with atomic capacity check. Excludes this attendee's current row from the capacity calculation.
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.
Upload an image to Bunny storage. Encrypts the image bytes before uploading. Returns the filename (without path) on success.
Upload raw bytes to storage, routing to local or Bunny based on config
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.
Validate an attachment file: check size only (any file type allowed).
Validate a custom domain (delegates to bunnyCdnApi for testability).
Validate a comma-separated list of host patterns. Returns null if all valid, or the first error message.
Validate encryption key is present and valid Call this on startup to fail fast if key is missing
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).
Validate a single host pattern Returns null if valid, or an error message if invalid
Validate an image file: check MIME type, size, and magic bytes.
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.
Validate a normalized slug. Returns error message or null.
Validate a Liquid template by parsing it (no rendering). Returns null if valid, or an error message string if invalid.
Verify a password against a hash Uses constant-time comparison to prevent timing attacks
Verify a signed CSRF token's signature and expiry
Verify a user's password (decrypt stored hash, then verify) Returns the decrypted password hash if valid (needed for KEK derivation)
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.
Wrap a checkout operation, converting PaymentUserError to { error } result and swallowing unexpected errors as null. Used by both provider adapters.
Ensure "email" is included in an event fields setting
Wrap a symmetric key with another key using AES-GCM Returns format: wk:1:$base64iv:$base64wrapped
Wrap a key using a session token (derives a wrapping key from the token)
Wrap a named resource so create/update apply demo overrides to the form
Encrypt closes_at for DB storage (null/empty → encrypted empty)
Encrypt event date for DB storage
Activity log entry
- active: boolean
- assign_built_site: boolean
- attachment_name: string
- attachment_url: string
- bookable_days: string[]
- can_pay_more: boolean
- closes_at: string | null
- created: string
- date: string
- description: string
- event_type: EventType
- fields: EventFields
- group_id: number
- hidden: boolean
- id: number
- image_url: string
- location: string
- max_attendees: number
- max_price: number
- max_quantity: number
- maximum_days_after: number
- minimum_days_before: number
- name: string
- non_transferable: boolean
- purchase_only: boolean
- slug: string
- slug_index: string
- thank_you_url: string
- unit_price: number
- webhook_url: string
Payment provider interface.
-
checkoutCompletedEventType: string
The webhook event type name that indicates a completed checkout
-
createCheckoutSession(): Promise<CheckoutSessionResult>intent: CheckoutIntent,baseUrl: string
Create a checkout session for one or more events. Returns a session ID and hosted checkout URL, or null on failure.
-
isPaymentRefunded(paymentReference: string): Promise<boolean>
Check if a payment has been refunded via the provider API. Used to refresh refund status from the edit attendee page.
-
refundPayment(paymentReference: string): Promise<boolean>
Refund a completed payment.
-
resolveWebhookSession(event: WebhookEvent): Promise<ValidatedPaymentSession | "skip" | null>
Resolve a validated session from a webhook event. Each provider knows how to extract/fetch session data from its own event structure, so the webhook handler stays provider-agnostic.
-
retrieveSession(sessionId: string): Promise<ValidatedPaymentSession | null>
Retrieve and validate a completed checkout session by ID. Returns the validated session or null if not found / invalid.
-
setupWebhookEndpoint(): Promise<WebhookSetupResult>secretKey: string,webhookUrl: string,existingEndpointId?: string | null
Set up a webhook endpoint for this provider. Some providers (e.g. Stripe) support programmatic creation.
-
type: PaymentProviderType
Provider identifier
-
verifyWebhookSignature(): Promise<WebhookVerifyResult>payload: string,signature: string,webhookUrl: string,payloadBytes: Uint8Array
Verify a webhook request's signature and parse the event payload.
Table definition with CRUD operations
-
deleteById: (id: InValue) => Promise<void>
Delete a row by primary key
-
findAll: () => Promise<Row[]>
Find all rows
-
findById: (id: InValue) => Promise<Row | null>
Find a row by primary key
-
fromDb: (row: Row) => Promise<Row>
Transform a row from DB (apply read transforms)
- inputKeyMap: Record<string, string>
-
insert: (input: Input) => Promise<Row>
Insert a new row, returns the created row
- name: string
- primaryKey: keyof Row & string
-
rowToInput: () => Partial<Input>row: Row,exclude?: readonly string[]
Build an Input object from an existing Row by copying the input-eligible columns and translating keys through
inputKeyMap. Lets callers spread a row into a new insert without restating every field. Columns named inexcludeare skipped — useful for auto-stamped fields likecreated. - schema: TableSchema<Row>
-
toDbValues: (input: Input | Partial<Input>) => Promise<Record<string, InValue>>
Transform input to DB values (apply write transforms and defaults)
-
update: () => Promise<Row | null>id: InValue,input: Partial<Input>
Update a row by primary key, returns updated row or null if not found
Aggregated statistics for active events
Activity log input for create
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.
Admin role levels
Session data needed by admin page templates
Attachment validation error
| { valid: false; error: AttachmentValidationError; }
Attachment validation result
& { paymentId?: string; bookings: EventBooking[]; }
Input for creating an attendee atomically (one or more events)
A single row in the attendee table (attendee + parent event context)
An attendee with all their event bookings (for token resolution)
-
bookings: EventAttendeeRow[]
Per-event bookings, sorted by start_at then event_id
- created: string
-
id: number
Base attendee fields (PII, token, created — shared across events)
- pii_blob: string
- ticket_token: string
- ticket_token_index: string
Item for batch availability check
& { date: string | null; items: BookingItem[]; eventAnswerIds?: Record<string, number[]>; }
Processed booking intent extracted from payment session metadata
Compact booking item stored in session metadata (serialized/deserialized as JSON)
| { type: "checkout"; checkoutUrl: string; }
| { type: "sold_out"; }
| { type: "checkout_failed"; error?: string; }
| { type: "creation_failed"; reason: "capacity_exceeded" | "encryption_error"; }
Booking result — callers map this to their response format
A single cache's stats snapshot
& { date: string | null; items: CheckoutItem[]; eventAnswerIds?: Record<string, number[]>; }
Registration intent for checkout (one or more events)
| { error: string; }
| null
Result of creating a checkout session.
Collection cache returned by collectionCache()
Column definition for a table
-
default: () => T
Default value generator (for created timestamps etc)
-
generated: boolean
Whether this column is auto-generated (like id)
-
read: (v: T) => Promise<T> | T
Transform value after reading from DB (e.g., decrypt)
-
write: (v: T) => Promise<T> | T
Transform value before writing to DB (e.g., encrypt)
Individual contact field name
& Partial<
Required name+email with optional phone/address/special_instructions from ContactInfo
Attendee contact details — the core PII fields collected at registration
| { success: false; reason: "capacity_exceeded" | "encryption_error"; }
Result of atomic attendee creation
Maps form field names to arrays of possible demo values
A base64-encoded email attachment
Attendee + event pair for email rendering
& { date: string; location: string; purchase_only: boolean; assign_built_site: boolean; }
Event data needed for registration pipeline (extends webhook event with display + assignment fields)
Union of all supported email provider keys, derived from the PROVIDERS map
Valid email template formats
Valid email template types
Error log context (privacy-safe metadata only)
-
attendeeId: number
Optional: attendee ID
-
code: ErrorCodeType
Error code for classification
-
detail: string
Optional: additional safe context
-
eventId: number
Optional: event ID (not slug)
Row from event_attendees — per-event booking data
A single event booking within a multi-event attendee creation
Contact fields setting for an event (comma-separated ContactField names, or empty for name-only). Alias kept for documentation; runtime enforcement happens in parseEventFields.
Event input fields for create/update (camelCase)
- active: boolean
- assignBuiltSite: boolean
- attachmentName: string
- attachmentUrl: string
- bookableDays: string[]
- canPayMore: boolean
- closesAt: string
- date: string
- description: string
- eventType: EventType
- fields: EventFields
- groupId: number
- hidden: boolean
- imageUrl: string
- location: string
- maxAttendees: number
- maxPrice: number
- maxQuantity: number
- maximumDaysAfter: number
- minimumDaysBefore: number
- name: string
- nonTransferable: boolean
- purchaseOnly: boolean
- slug: string
- slugIndex: string
- thankYouUrl: string
- unitPrice: number
- webhookUrl: string
Event type: standard (one-time) or daily (date-based booking)
Result type for event + activity log batch query
Result type for event + single attendee query
Result type for combined event + attendees query
Group input fields for create/update (camelCase)
Holiday input fields for create/update (camelCase)
Image validation error
& [K in OptionalInputKeys<Row, Schema>]?: Row[K]
Derive Input type from Row type and Schema
Data needed to generate a pass — maps to existing ticket/event data
-
attendeeDate: string | null
Selected date for daily/recurring events (null for one-off events)
- backgroundColor: string
-
checkinUrl: string
Full URL encoded in the QR barcode
- currencyCode: string
-
description: string
VoiceOver accessibility description for the pass
-
eventDate: string
ISO 8601 date used for relevantDate and secondary field
-
eventLocation: string
Venue shown in secondary field
-
eventName: string
Event name displayed in the primary field
-
foregroundColor: string
Optional pass colors (CSS rgb() format)
- labelColor: string
-
organizationName: string
Platform/domain name shown on the pass header
- pricePaid: number
-
quantity: number
Ticket quantity and price (in minor units, e.g. pence)
-
serialNumber: string
Unique token identifying this ticket
-
webServiceURL: string
Base URL for Apple Wallet web service (e.g. https://example.com)
Supported payment provider identifiers
Valid payment status values
Short keys used in the PII blob JSON to minimize encrypted payload size
| { ok: false; error: string; }
Result type for price validation
A single logged query
Registration entry: event + attendee pair
| { reserved: false; existing: ProcessedPayment; }
Result of session reservation attempt
Result of a seed operation
Metadata attached to a validated payment session.
Full settings snapshot type.
Slug-with-index pair
Union of all string-setting snapshot keys.
| "eventDate"
| "eventLocation"
| "attendeeDate"
| "quantity"
| "checkinUrl"
& { pricePaid: string; currency: string; purchaseOnly?: boolean; }
Non-PII ticket data for SVG rendering (extends shared wallet fields with display-formatted values)
Table schema definition Keys are DB column names (snake_case), values are column definitions
UI theme
Input for updating attendee PII (shared across events)
- address: string
- email: string
- name: string
-
payment_id: string
Decrypted payment_id for PII blob rebuild (from existing attendee)
- phone: string
- special_instructions: string
-
ticket_token: string
Decrypted ticket_token for PII blob rebuild (from existing attendee)
Input for updating a single event link
| { success: false; reason: "capacity_exceeded"; }
Result of updating an event link
A validated payment session returned after checkout completion
-
amountTotal: number
Total amount charged in smallest currency unit (cents), from the payment provider
- id: string
- metadata: SessionMetadata
- paymentReference: string
- paymentStatus: PaymentStatus
& { id: number; quantity: number; payment_id: string; price_paid: string; ticket_token: string; date: string | null; }
Attendee data needed for webhook notifications
| { success: false; error: string; }
Result of webhook endpoint setup
| { valid: false; error: string; }
Result of webhook signature verification
Activity log table definition message is encrypted - decrypted only for admin view
Example availability response JSON
Example free booking response JSON
Example paid booking response JSON
Example booking request body
Example event matching the webhook example data
The example PublicEvent, produced by toPublicEvent
Example list response JSON
Example single-event response JSON
User-facing messages for attachment validation errors
Attendee PII fields
SELECT clause for attendee + event_attendees JOINs (INNER JOIN context).
Derives date from start_at for backward compatibility with the Attendee type.
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).
Stubbable API for testing atomic operations
Shared failure result for capacity-exceeded
Helper to create column definitions
-
boolean: (defaultValue: boolean) => ColumnDef<boolean>
Boolean column stored as INTEGER 0/1 in the database
-
converted: <App>(config: { default?: () => App; write: (v: App) => InValue; read: (raw: InValue) => App; }) => ColumnDef<App>
Column with type conversion between app and DB representations
-
encrypted: <T>() => ColumnDef<T>encrypt: ColumnTransform<T>,decrypt: ColumnTransform<T>
Column with read/write transforms (e.g., for encryption)
-
encryptedNullable: <T>(def: ColumnDef<T>) => ColumnDef<T | null>
Wrap an existing encrypted column def to pass through null values
-
encryptedText: () => ColumnDef<string>encrypt: ColumnTransform<string>,decrypt: ColumnTransform<string>
Encrypted text column with empty-string default
-
generated: <T>() => ColumnDef<T>
Auto-generated column (like id)
-
simple: <T>() => ColumnDef<T>
Simple column with no special handling
-
transform: <T>() => ColumnDef<T>write: (v: T) => Promise<T> | T,read: (v: T) => Promise<T> | T
Column with custom transforms
-
withDefault: <T>(defaultFn: () => T) => ColumnDef<T>
Column with default value
- 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
All valid contact field names (runtime array matching the ContactField union)
Default message for invalid/expired CSRF form submissions
| "Monday"
| "Tuesday"
| "Wednesday"
| "Thursday"
| "Friday"
| "Saturday"
Day name lookup from Date.getUTCDay() index (Sunday=0)
Default bookable days (all days of the week)
Default timezone when none is configured
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.
Demo event locations — pretend rock-venue / festival listings. Procedurally generated from the venue word pools using a seeded PRNG.
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.
Matches a valid hostname like "example.com" or "sub.example.com"
Display labels for email providers — keys must match EmailProvider
Error code strings for use in logError calls
Human-readable labels for error codes (shown in admin activity log)
Event metadata fields
Example inputs used by both the fixture and the test
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.
Group name and description fields
Groups table with CRUD operations — writes auto-invalidate the cache
Holiday name field
Holidays table with CRUD operations — writes auto-invalidate the cache
User-facing messages for image validation errors
Type guard: check if a string is a valid AdminLevel
Type guard: check if an arbitrary string is a valid ContactField
Type guard: check if an arbitrary string is a valid EventType
Type guard: check if a string is a valid PaymentProviderType
Join an array of strings into a single string (curried reduce shorthand). Replaces the common pattern: reduce((acc: string, s: string) => acc + s, "")
Maximum attachment file size in bytes (default: 25MB)
Stubbable API for internal calls (testable via spyOn, like stripeApi/squareApi)
Current PII blob schema version
Execute multiple read queries in a single round-trip using Turso batch API.
Ordered table names — matches FK dependency order (parents before children)
Max attendees per seeded event
- appleWallet
- attendeeColumnOrder(): string
- bookingFee(): string
- bunnySubdomain(): string
- businessEmail(): string
-
clearTestOverride(...keys: (keyof SettingsData)[]): void
Remove specific test override keys (falls back to data).
-
clearTestOverrides(): void
Clear all test overrides.
- contactPageText(): string
- country(): string
- currency(): string
- currentTask(): string
- customDomain(): string
- customDomainLastValidated(): string
-
email: { get apiKey(): string; get fromAddress(): string; get hasApiKey(): boolean; get provider(): string; template(): string; templateSet(type: EmailTemplateType): { subject: string; html: string; text: string; }; }type: EmailTemplateType,format: EmailTemplateFormat
- embedHosts(): string
- eventColumnOrder(): string
-
getCachedRaw
Read a raw (possibly encrypted) value from the cache.
- googleWallet
- headerImageUrl(): string
- homepageText(): string
- invalidateCache
- lastPrunedLogins(): string
- lastPrunedPayments(): string
- lastPrunedSessions(): string
- lastPrunedTokens(): string
- latestScriptVersion(): string
- latestScriptVersionName(): string
- loadAll
- paymentProvider(): PaymentProviderType | null
- phonePrefix(): string
- publicKey(): string
-
setForTest(overrides: Partial<SettingsData>): void
Set test overrides (survive invalidateCache, cleared by clearTestOverrides).
-
setRaw
Write a raw value to the DB (low-level, prefer update.*).
- setup: { clearCache; complete; isComplete; }
- showPublicApi(): boolean
- showPublicSite(): boolean
- 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; }
- terms(): string
- theme(): Theme
- timezone(): 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: () => 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; }type: EmailTemplateType,format: EmailTemplateFormat,content: string
- updateUserPassword
- websiteTitle(): string
- withCurrentTask
- wrappedPrivateKey(): string
Site contact page fields
Site homepage fields
Square metadata constraint: each value max 255 characters
Threshold for abandoned payment reservations in ms (default: 300000 = 5 min)
Stripe metadata constraint: each value max 500 characters
Terms and conditions field
Valid provider names, derived from the PROVIDERS map
Decoded icon files for inclusion in .pkpass bundles
Pretty-printed JSON for embedding in documentation
The example payload, matching what buildWebhookPayload would produce
Database client, ORM abstractions, and entity tables.
Activate a user by wrapping the data key with their KEK
Add a new event link for an existing attendee with atomic capacity check. Does NOT create a new attendee or touch PII — just inserts an event_attendees row.
Record a query (no-op when logging is disabled)
Assign events to a group by updating their group_id.
Build an INSERT statement for the attendees table from encrypted fields.
Build a capacity-checked INSERT into event_attendees.
Build input key mapping from DB columns snake_case DB column → camelCase input key
Build a PII blob JSON from contact fields
Wrapper for test mocking - delegates to attendeesApi at runtime
Check a capacity-guarded write result and invalidate cache on success
Clear login attempts for an IP (on successful login)
Clear stored ticket tokens for a session (after redirect has consumed them)
Compute slug index from slug for blind index lookup
Compute slug index from slug for blind index lookup
Extract ContactInfo fields from an object
Wrapper for test mocking - delegates to attendeesApi at runtime
Create an invited user (no password yet, has invite code)
Create a new session with CSRF token, wrapped data key, and user ID Token is hashed before storage for security
Create a new user with encrypted fields
Convert nullable date to start_at/end_at (null-safe wrapper around dateToRange)
Decrypt a user's admin level
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.
Decrypt a single raw attendee, handling null input. Used when attendee is fetched via batch query.
Decrypt a list of raw attendees (all fields). Used when attendees are fetched via batch query.
Decrypt the ticket_tokens field from a processed payment record. Returns the plaintext token string (e.g. "tok1+tok2") or empty string.
Decrypt a user's username
Helper for tables whose primary key column is id.
Define a table with CRUD operations
Delete all sessions (used when password is changed)
Delete all stale reservations (unfinalized and older than STALE_RESERVATION_MS). Called from admin event views to clean up abandoned checkouts.
Delete an attendee and all its event links, payments, and answers.
Delete rows matching a field value
Delete rows from multiple tables in a single batch transaction
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.
Delete all sessions except the current one Token is hashed before database comparison
Delete a session by token Token is hashed before database lookup
Delete a stale reservation to allow retry
Delete a user and all their sessions and API keys
Enable query logging and clear previous entries
Encrypt attendee fields into a PII blob, returning null if key not configured
Shared encrypted name column for tables that store a display name.
Encrypt a PII blob JSON string with the public key
Execute multiple write statements, discarding results.
Finalize a reserved session with the created attendee ID (second phase)
Get active events in a group with attendee counts.
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.
Get active holidays (end_date >= today) for date computation (from cache). "today" is computed in the configured timezone.
Get all activity log entries (most recent first)
Get all daily events with attendee counts (from cache).
Get all events with attendee counts (from cache)
Get all groups, decrypted, ordered by id (from cache)
Get all holidays, decrypted, ordered by start_date (from cache)
Get all sessions ordered by expiration (newest first)
Get all standard events with attendee counts (from cache). Used by the calendar view to include one-time events on their scheduled date.
Get all users (for admin user management page, from cache)
Get an attendee by ID (decrypted) Requires private key for decryption - only available to authenticated sessions
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)
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.
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.
Get attendees for an event without decrypting PII Used for tests and operations that don't need decrypted data
Get distinct attendee dates for daily events. Used for the calendar date picker (lightweight, no attendee data).
Get raw attendees for daily events on a specific date. Bounded query: only returns attendees matching the given date.
Get the total attendee quantity for a specific event + date
Get or create database client
Get a single event by ID (from cache)
Get activity log entries for an event (most recent first)
Get all events in a group with attendee counts (including inactive).
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.
Get event and its activity log in a single database round-trip. Uses batch API to reduce latency for remote databases.
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.
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.
Get event with attendee count (from cache)
Get event with attendee count by slug (from cache)
Get a single group by slug_index (from cache)
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.
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.
Returns undefined when no group cap applies: ungrouped, uncapped
group, or daily event without a date.
Get the newest attendees across all events without decrypting PII. Used for the admin dashboard to show recent registrations.
Get the attendee ID for an already-processed session Used to return success for idempotent webhook retries
Return a snapshot of all logged queries
Return the start time recorded by enableQueryLog()
Get a session by token (with 10s TTL cache) Token is hashed for database lookup
Get ungrouped events (group_id = 0) with attendee counts.
Get a user by ID (from cache)
Find a user by invite code hash Scans all users, decrypts invite_code_hash, and compares
Look up a user by username (using blind index, from cache)
Wrapper for test mocking - delegates to attendeesApi at runtime
Hash an invite code using SHA-256
Check if a user has set their password (password_hash is non-empty encrypted value)
Shared columns for tables with encrypted slug + blind-index slug_index.
Increment the attachment download counter for an attendee. Uses atomic SQL increment to avoid race conditions.
Initialize database tables — idempotent, safe to call on every startup. Uses an advisory lock to prevent concurrent migrations.
Build SQL placeholders for an IN clause, e.g. "?, ?, ?"
Build an INSERT statement from a table name and column→value record.
Invalidate the events cache (for testing or after writes).
Invalidate the groups cache (for testing or after writes).
Invalidate the holidays cache (for testing or after writes).
Invalidate the users cache (for testing or after writes).
Check if a group slug is already in use. Checks both events and groups for cross-table uniqueness.
Check if a user's invite has expired. Callers should skip this for users who have already set a password.
Check if a user's invite is still valid (not expired, has invite code)
Check if IP is rate limited for login
Whether query logging is currently active
Check if a reservation is stale (abandoned by a crashed process)
Check if a payment session has already been processed
Check if a slug is already in use (optionally excluding a specific event ID) Uses slug_index for lookup (blind index)
Check if a username is already taken
Mark an attendee as refunded for a specific event. Keeps payment_id intact so payment details can still be viewed.
Parse a PII blob JSON back into contact fields (defaults v to 1 for pre-versioned blobs)
Query all rows, returning a typed array
Execute a SQL query and map result rows through an async transformer.
Query single row, returning null if not found
Embed a raw SQL expression (e.g. last_insert_rowid())
Record a failed login attempt Returns true if the account is now locked
Register a cache stat provider (called at module load time)
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.
Reset the database by dropping all tables (reverse order for FK safety)
Reset group assignment on all events in a group.
Clear session cache (exported for testing)
Cast libsql ResultSet rows to a typed array (single centralized assertion)
Set database client (for testing)
Set the active flag on every event in a group.
Returns the number of events affected.
Set a user's password (for invite flow)
Convert snake_case to camelCase
Convert camelCase to snake_case
Run an async DB operation and log it when tracking is active
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.
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).
Update an attendee's checked_in status for a specific event. Caller must be authenticated admin (public key always exists after setup)
Update a single event link's quantity and date with atomic capacity check. Excludes this attendee's current row from the capacity calculation.
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).
Verify a user's password (decrypt stored hash, then verify) Returns the decrypted password hash if valid (needed for KEK derivation)
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.
Encrypt closes_at for DB storage (null/empty → encrypted empty)
Encrypt event date for DB storage
Activity log entry
Table definition with CRUD operations
-
deleteById: (id: InValue) => Promise<void>
Delete a row by primary key
-
findAll: () => Promise<Row[]>
Find all rows
-
findById: (id: InValue) => Promise<Row | null>
Find a row by primary key
-
fromDb: (row: Row) => Promise<Row>
Transform a row from DB (apply read transforms)
- inputKeyMap: Record<string, string>
-
insert: (input: Input) => Promise<Row>
Insert a new row, returns the created row
- name: string
- primaryKey: keyof Row & string
-
rowToInput: () => Partial<Input>row: Row,exclude?: readonly string[]
Build an Input object from an existing Row by copying the input-eligible columns and translating keys through
inputKeyMap. Lets callers spread a row into a new insert without restating every field. Columns named inexcludeare skipped — useful for auto-stamped fields likecreated. - schema: TableSchema<Row>
-
toDbValues: (input: Input | Partial<Input>) => Promise<Record<string, InValue>>
Transform input to DB values (apply write transforms and defaults)
-
update: () => Promise<Row | null>id: InValue,input: Partial<Input>
Update a row by primary key, returns updated row or null if not found
Aggregated statistics for active events
Activity log input for create
& { paymentId?: string; bookings: EventBooking[]; }
Input for creating an attendee atomically (one or more events)
An attendee with all their event bookings (for token resolution)
-
bookings: EventAttendeeRow[]
Per-event bookings, sorted by start_at then event_id
- created: string
-
id: number
Base attendee fields (PII, token, created — shared across events)
- pii_blob: string
- ticket_token: string
- ticket_token_index: string
Item for batch availability check
Column definition for a table
-
default: () => T
Default value generator (for created timestamps etc)
-
generated: boolean
Whether this column is auto-generated (like id)
-
read: (v: T) => Promise<T> | T
Transform value after reading from DB (e.g., decrypt)
-
write: (v: T) => Promise<T> | T
Transform value before writing to DB (e.g., encrypt)
| { success: false; reason: "capacity_exceeded" | "encryption_error"; }
Result of atomic attendee creation
Valid email template formats
Valid email template types
Row from event_attendees — per-event booking data
A single event booking within a multi-event attendee creation
Event input fields for create/update (camelCase)
- active: boolean
- assignBuiltSite: boolean
- attachmentName: string
- attachmentUrl: string
- bookableDays: string[]
- canPayMore: boolean
- closesAt: string
- date: string
- description: string
- eventType: EventType
- fields: EventFields
- groupId: number
- hidden: boolean
- imageUrl: string
- location: string
- maxAttendees: number
- maxPrice: number
- maxQuantity: number
- maximumDaysAfter: number
- minimumDaysBefore: number
- name: string
- nonTransferable: boolean
- purchaseOnly: boolean
- slug: string
- slugIndex: string
- thankYouUrl: string
- unitPrice: number
- webhookUrl: string
Result type for event + activity log batch query
Result type for event + single attendee query
Result type for combined event + attendees query
Group input fields for create/update (camelCase)
Holiday input fields for create/update (camelCase)
& [K in OptionalInputKeys<Row, Schema>]?: Row[K]
Derive Input type from Row type and Schema
A single logged query
| { reserved: false; existing: ProcessedPayment; }
Result of session reservation attempt
Full settings snapshot type.
Union of all string-setting snapshot keys.
Table schema definition Keys are DB column names (snake_case), values are column definitions
Input for updating attendee PII (shared across events)
- address: string
- email: string
- name: string
-
payment_id: string
Decrypted payment_id for PII blob rebuild (from existing attendee)
- phone: string
- special_instructions: string
-
ticket_token: string
Decrypted ticket_token for PII blob rebuild (from existing attendee)
Input for updating a single event link
| { success: false; reason: "capacity_exceeded"; }
Result of updating an event link
Activity log table definition message is encrypted - decrypted only for admin view
SELECT clause for attendee + event_attendees JOINs (INNER JOIN context).
Derives date from start_at for backward compatibility with the Attendee type.
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).
Stubbable API for testing atomic operations
Shared failure result for capacity-exceeded
Helper to create column definitions
-
boolean: (defaultValue: boolean) => ColumnDef<boolean>
Boolean column stored as INTEGER 0/1 in the database
-
converted: <App>(config: { default?: () => App; write: (v: App) => InValue; read: (raw: InValue) => App; }) => ColumnDef<App>
Column with type conversion between app and DB representations
-
encrypted: <T>() => ColumnDef<T>encrypt: ColumnTransform<T>,decrypt: ColumnTransform<T>
Column with read/write transforms (e.g., for encryption)
-
encryptedNullable: <T>(def: ColumnDef<T>) => ColumnDef<T | null>
Wrap an existing encrypted column def to pass through null values
-
encryptedText: () => ColumnDef<string>encrypt: ColumnTransform<string>,decrypt: ColumnTransform<string>
Encrypted text column with empty-string default
-
generated: <T>() => ColumnDef<T>
Auto-generated column (like id)
-
simple: <T>() => ColumnDef<T>
Simple column with no special handling
-
transform: <T>() => ColumnDef<T>write: (v: T) => Promise<T> | T,read: (v: T) => Promise<T> | T
Column with custom transforms
-
withDefault: <T>(defaultFn: () => T) => ColumnDef<T>
Column with default value
- 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
Default bookable days (all days of the week)
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.
Groups table with CRUD operations — writes auto-invalidate the cache
Holidays table with CRUD operations — writes auto-invalidate the cache
Current PII blob schema version
Execute multiple read queries in a single round-trip using Turso batch API.
Ordered table names — matches FK dependency order (parents before children)
- appleWallet
- attendeeColumnOrder(): string
- bookingFee(): string
- bunnySubdomain(): string
- businessEmail(): string
-
clearTestOverride(...keys: (keyof SettingsData)[]): void
Remove specific test override keys (falls back to data).
-
clearTestOverrides(): void
Clear all test overrides.
- contactPageText(): string
- country(): string
- currency(): string
- currentTask(): string
- customDomain(): string
- customDomainLastValidated(): string
-
email: { get apiKey(): string; get fromAddress(): string; get hasApiKey(): boolean; get provider(): string; template(): string; templateSet(type: EmailTemplateType): { subject: string; html: string; text: string; }; }type: EmailTemplateType,format: EmailTemplateFormat
- embedHosts(): string
- eventColumnOrder(): string
-
getCachedRaw
Read a raw (possibly encrypted) value from the cache.
- googleWallet
- headerImageUrl(): string
- homepageText(): string
- invalidateCache
- lastPrunedLogins(): string
- lastPrunedPayments(): string
- lastPrunedSessions(): string
- lastPrunedTokens(): string
- latestScriptVersion(): string
- latestScriptVersionName(): string
- loadAll
- paymentProvider(): PaymentProviderType | null
- phonePrefix(): string
- publicKey(): string
-
setForTest(overrides: Partial<SettingsData>): void
Set test overrides (survive invalidateCache, cleared by clearTestOverrides).
-
setRaw
Write a raw value to the DB (low-level, prefer update.*).
- setup: { clearCache; complete; isComplete; }
- showPublicApi(): boolean
- showPublicSite(): boolean
- 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; }
- terms(): string
- theme(): Theme
- timezone(): 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: () => 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; }type: EmailTemplateType,format: EmailTemplateFormat,content: string
- updateUserPassword
- websiteTitle(): string
- withCurrentTask
- wrappedPrivateKey(): string
Threshold for abandoned payment reservations in ms (default: 300000 = 5 min)
Encryption, hashing, CSRF protection, and secure operations.
AES-GCM decrypt raw data, returning the decrypted ArrayBuffer
AES-GCM encrypt raw data, returning IV and ciphertext bytes
Convert standard base64 to base64url (no padding). Works on both strings and Uint8Array (bytes are first encoded to base64).
Compute HMAC-SHA256 using Web Crypto API, returning raw ArrayBuffer
Compute ticket token index using HMAC for blind lookups Similar to slug_index for events - allows lookup without decrypting
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.
Decrypt a string value encrypted with encrypt() Expects format: enc:1:$base64iv:$base64ciphertext
Decrypt attendee PII using the private key Used in admin views after obtaining private key from session
Decrypt binary data encrypted with encryptBytes(). Expects ENCB binary format: magic + version + IV + ciphertext.
Decrypt data with a symmetric key
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
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)
Encrypt attendee PII using the public key from settings This can be called without authentication (for public ticket forms)
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).
Encrypt data with a symmetric key (for wrapping private key with DATA_KEY)
Format IV + ciphertext as a prefixed base64 string
Convert base64 string to Uint8Array
Generate a random 256-bit symmetric key for data encryption
Generate an RSA key pair for asymmetric encryption Returns { publicKey, privateKey } as exportable JWK strings
Generate a cryptographically secure random token Uses Web Crypto API getRandomValues
Generate a 5-byte uppercase hex ticket token for public ticket URLs
Get the most recently generated CSRF token (for synchronous JSX rendering)
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
Derive the private key from session credentials Used to decrypt attendee PII in admin views Results are cached per session token for 10 seconds
Generate random bytes using Web Crypto API
Hash a password using PBKDF2 Returns format: pbkdf2:iterations:$base64salt:$base64hash
Hash a session token using SHA-256 Used to store session lookups without exposing the actual token
HMAC-SHA256 hash using DB_ENCRYPTION_KEY Used for blind indexes and hashing limited keyspace values Returns deterministic output for same input (unlike encrypt)
Convert ArrayBuffer to base64 string
Convert ArrayBuffer to hex string
Decrypt data using hybrid encryption Expects format: hyb:1:$base64WrappedKey:$base64iv:$base64ciphertext Results are cached in a bounded LRU (ciphertext -> plaintext)
Encrypt data using hybrid encryption (RSA + AES)
Import a CryptoKey from DB_ENCRYPTION_KEY.
Import a private key from JWK string
Import a public key from JWK string
Check whether a token uses the signed format
Parse a prefixed encrypted payload into IV and ciphertext bytes. Validates the prefix and separator; throws on invalid format.
Constant-time string comparison to prevent timing attacks
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).
Explicitly enable/disable fast PBKDF2 for testing without env var races
Explicitly set RSA key size for testing without env var races
Create a signed CSRF token: s1.{timestamp}.{nonce}.{hmac}
Decrypt a prefixed AES-GCM payload with the given key.
Encrypt plaintext with an AES-GCM key, returning prefixed format: enc:1:$base64iv:$base64ciphertext
Convert Uint8Array to base64 string
Unwrap a symmetric key Expects format: wk:1:$base64iv:$base64wrapped
Unwrap a key using a session token
Validate encryption key is present and valid Call this on startup to fail fast if key is missing
Verify a password against a hash Uses constant-time comparison to prevent timing attacks
Verify a signed CSRF token's signature and expiry
Wrap a symmetric key with another key using AES-GCM Returns format: wk:1:$base64iv:$base64wrapped
Wrap a key using a session token (derives a wrapping key from the token)
Default message for invalid/expired CSRF form submissions
Payment processing with Stripe and Square.
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.
Build checkout metadata from a CheckoutIntent (converts items to compact form).
Build checkout session metadata from booking data (items already compact).
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.
Enforce metadata value length limits for a payment provider.
Extract a human-readable message from an unknown caught value
Normalize validated session metadata into the canonical SessionMetadata shape.
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.
Validate that session metadata contains required fields (name + items).
Type guard: check if a string is a valid PaymentStatus
Safely execute async operation, returning null on error. Re-throws PaymentUserError so user-facing messages propagate.
Convert single-event answerIds to the per-event format used in metadata
Convert registration line items to compact booking items
Convert a provider-specific checkout result to a CheckoutSessionResult. Returns null if session ID or URL is missing.
Wrap a checkout operation, converting PaymentUserError to { error } result and swallowing unexpected errors as null. Used by both provider adapters.
Payment provider interface.
-
checkoutCompletedEventType: string
The webhook event type name that indicates a completed checkout
-
createCheckoutSession(): Promise<CheckoutSessionResult>intent: CheckoutIntent,baseUrl: string
Create a checkout session for one or more events. Returns a session ID and hosted checkout URL, or null on failure.
-
isPaymentRefunded(paymentReference: string): Promise<boolean>
Check if a payment has been refunded via the provider API. Used to refresh refund status from the edit attendee page.
-
refundPayment(paymentReference: string): Promise<boolean>
Refund a completed payment.
-
resolveWebhookSession(event: WebhookEvent): Promise<ValidatedPaymentSession | "skip" | null>
Resolve a validated session from a webhook event. Each provider knows how to extract/fetch session data from its own event structure, so the webhook handler stays provider-agnostic.
-
retrieveSession(sessionId: string): Promise<ValidatedPaymentSession | null>
Retrieve and validate a completed checkout session by ID. Returns the validated session or null if not found / invalid.
-
setupWebhookEndpoint(): Promise<WebhookSetupResult>secretKey: string,webhookUrl: string,existingEndpointId?: string | null
Set up a webhook endpoint for this provider. Some providers (e.g. Stripe) support programmatic creation.
-
type: PaymentProviderType
Provider identifier
-
verifyWebhookSignature(): Promise<WebhookVerifyResult>payload: string,signature: string,webhookUrl: string,payloadBytes: Uint8Array
Verify a webhook request's signature and parse the event payload.
& { date: string | null; items: BookingItem[]; eventAnswerIds?: Record<string, number[]>; }
Processed booking intent extracted from payment session metadata
Compact booking item stored in session metadata (serialized/deserialized as JSON)
| { type: "checkout"; checkoutUrl: string; }
| { type: "sold_out"; }
| { type: "checkout_failed"; error?: string; }
| { type: "creation_failed"; reason: "capacity_exceeded" | "encryption_error"; }
Booking result — callers map this to their response format
& { date: string | null; items: CheckoutItem[]; eventAnswerIds?: Record<string, number[]>; }
Registration intent for checkout (one or more events)
| { error: string; }
| null
Result of creating a checkout session.
Supported payment provider identifiers
Supported payment provider identifiers
Valid payment status values
Metadata attached to a validated payment session.
A validated payment session returned after checkout completion
-
amountTotal: number
Total amount charged in smallest currency unit (cents), from the payment provider
- id: string
- metadata: SessionMetadata
- paymentReference: string
- paymentStatus: PaymentStatus
| { success: false; error: string; }
Result of webhook endpoint setup
| { valid: false; error: string; }
Result of webhook signature verification
Stubbable API for internal calls (testable via spyOn, like stripeApi/squareApi)
Square metadata constraint: each value max 255 characters
Stripe metadata constraint: each value max 500 characters
Email sending, templates, and notifications.
Build SVG ticket data from an email entry (non-PII only)
Build the data object exposed to Liquid templates
Generate SVG ticket attachments for all entries
Read email config from DB settings. Falls back to business email for fromAddress. Returns null if not configured.
Get host-level email config. Uses test override if set, otherwise reads env vars.
Type guard: checks if a string is a valid EmailProvider
Validates a basic email format: something@something.something
Normalizes email: trim and lowercase
Render all 3 parts (subject, html, text) using custom templates with fallback to defaults
Render a single Liquid template string with the given data
For testing: reset the engine (so filters can be re-registered after currency changes)
For testing: reset host email config to read from env vars.
Send a single email via the configured provider. Logs errors, never throws. Returns HTTP status or undefined on non-HTTP errors.
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.
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.
Send a test email to the business email address. Returns HTTP status or undefined on non-HTTP errors.
For testing: set host email config directly. Bypasses env vars to avoid races.
Updates the business email in the database. Pass empty string to clear the business email. Email is encrypted at rest.
Validate a Liquid template by parsing it (no rendering). Returns null if valid, or an error message string if invalid.
A base64-encoded email attachment
Attendee + event pair for email rendering
& { date: string; location: string; purchase_only: boolean; assign_built_site: boolean; }
Event data needed for registration pipeline (extends webhook event with display + assignment fields)
Union of all supported email provider keys, derived from the PROVIDERS map
Display labels for email providers — keys must match EmailProvider
Valid provider names, derived from the PROVIDERS map
Ticket generation: QR codes, SVG tickets, and Apple Wallet passes.
Build the check-in URL for a single ticket token
Build info lines from ticket data (non-PII event and booking details)
Build a complete .pkpass file as a Uint8Array (ZIP archive)
Create manifest.json mapping filenames to SHA-1 hashes
Whether this event can send QR code scanners directly to checkout. True when no extra contact fields or questions are required.
Extract the inner content of an SVG element (strip the outer <svg> wrapper)
Extract the viewBox from an SVG element to compute its coordinate space
Build the pass.json content from pass data and signing credentials
Generate an SVG string for a QR code encoding the given text. Returns a complete <svg> element suitable for inline embedding.
Generate a standalone SVG ticket with QR code and event/booking details. Returns a complete SVG document string.
Validate that a string is a parseable PEM certificate
Validate that a string is a parseable PEM private key
Pad a serial number to meet Apple's minimum authenticationToken length. Uses "-" (not in uppercase hex charset) so padding is cleanly reversible.
Compute SHA-1 hex digest of a Uint8Array
Sign the manifest with PKCS#7 detached signature
Strip padding added by padAuthToken to recover the original serial number
Data needed to generate a pass — maps to existing ticket/event data
-
attendeeDate: string | null
Selected date for daily/recurring events (null for one-off events)
- backgroundColor: string
-
checkinUrl: string
Full URL encoded in the QR barcode
- currencyCode: string
-
description: string
VoiceOver accessibility description for the pass
-
eventDate: string
ISO 8601 date used for relevantDate and secondary field
-
eventLocation: string
Venue shown in secondary field
-
eventName: string
Event name displayed in the primary field
-
foregroundColor: string
Optional pass colors (CSS rgb() format)
- labelColor: string
-
organizationName: string
Platform/domain name shown on the pass header
- pricePaid: number
-
quantity: number
Ticket quantity and price (in minor units, e.g. pence)
-
serialNumber: string
Unique token identifying this ticket
-
webServiceURL: string
Base URL for Apple Wallet web service (e.g. https://example.com)
| "eventDate"
| "eventLocation"
| "attendeeDate"
| "quantity"
| "checkinUrl"
& { pricePaid: string; currency: string; purchaseOnly?: boolean; }
Non-PII ticket data for SVG rendering (extends shared wallet fields with display-formatted values)
Decoded icon files for inclusion in .pkpass bundles
Event management: fields, sorting, and availability.
Add days to a YYYY-MM-DD date string
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).
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.
Format a YYYY-MM-DD date for display. Returns "Monday 15 March 2026"
Format an ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
Compact ISO datetime formatter for table cells. Returns e.g. "07/04/2026 14:00" in the configured timezone.
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.
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.
Load all events with holidays and return them sorted, filtered by predicate.
Determine which contact fields to collect for multiple events. Returns the union of all field settings, sorted by canonical CONTACT_FIELDS order.
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.
Parse a comma-separated fields string into individual ContactField names
Sort events in unified 3-tier order. Works with any Event subtype (Event, EventWithCount, etc.).
Round a date down to the start of the current hour for cache-stable signatures
Ensure "email" is included in an event fields setting
| "Monday"
| "Tuesday"
| "Wednesday"
| "Thursday"
| "Friday"
| "Saturday"
Day name lookup from Date.getUTCDay() index (Sunday=0)
Configuration, environment, and session context.
Create a type guard from a readonly array of string literal values
Get booking fee percentage from database. Returns 0 if not set.
Get the Bunny CDN API key from environment
Get the Bunny DNS subdomain suffix (e.g. ".tickets") from environment
Get the Bunny DNS zone ID from environment
Get the Bunny Edge Script ID from environment
Return the cached session if already resolved, or undefined if not yet resolved
Get the effective domain synchronously (must call loadEffectiveDomain first).
Get allowed embed hosts from database (encrypted, parsed to array) Returns empty array if not configured (embedding allowed from anywhere)
Get an environment variable value Checks process.env first (Bunny Edge), falls back to Deno.env (local dev)
Check if Bunny CDN pull zone management is enabled Requires both BUNNY_API_KEY and BUNNY_SCRIPT_ID to be set
Check if Bunny DNS subdomain feature is enabled. Requires BUNNY_API_KEY and BUNNY_DNS_ZONE_ID to be set.
Whether an event can accept payments (has a price or allows pay-what-you-want)
Check if payments are enabled (any provider configured with valid keys)
Check if the system is in read-only mode (READ_ONLY env var)
Load the effective domain from DB, falling back to the request URL hostname.
Parse a flash cookie value into type, message, and optional result
Get a required environment variable, throwing if not set.
Use this instead of getEnv(key) as string when the variable must exist.
Reset effective domain cache (for testing).
Run a function within a session-memoization scope
Store the resolved session in the current request scope
Set effective domain directly (for testing).
- active: boolean
- assign_built_site: boolean
- attachment_name: string
- attachment_url: string
- bookable_days: string[]
- can_pay_more: boolean
- closes_at: string | null
- created: string
- date: string
- description: string
- event_type: EventType
- fields: EventFields
- group_id: number
- hidden: boolean
- id: number
- image_url: string
- location: string
- max_attendees: number
- max_price: number
- max_quantity: number
- maximum_days_after: number
- minimum_days_before: number
- name: string
- non_transferable: boolean
- purchase_only: boolean
- slug: string
- slug_index: string
- thank_you_url: string
- unit_price: number
- webhook_url: string
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.
Admin role levels
Session data needed by admin page templates
A single row in the attendee table (attendee + parent event context)
Individual contact field name
& Partial<
Required name+email with optional phone/address/special_instructions from ContactInfo
Attendee contact details — the core PII fields collected at registration
Contact fields setting for an event (comma-separated ContactField names, or empty for name-only). Alias kept for documentation; runtime enforcement happens in parseEventFields.
Event type: standard (one-time) or daily (date-based booking)
Supported payment provider identifiers
Short keys used in the PII blob JSON to minimize encrypted payload size
UI theme
All valid contact field names (runtime array matching the ContactField union)
Type guard: check if a string is a valid AdminLevel
Type guard: check if an arbitrary string is a valid ContactField
Type guard: check if an arbitrary string is a valid EventType
Type guard: check if a string is a valid PaymentProviderType
Shared utilities: FP helpers, formatting, slugs, caching, and logging.
Queue a promise that must complete before the response is sent
Narrow an unknown value to string, defaulting to "" if not a string.
Replaces typeof x === "string" ? x : "" at type boundaries.
Resource management pattern (like Haskell's bracket or try-with-resources). Ensures cleanup happens even if the operation throws.
Split an array into chunks of a given size
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.
Remove null and undefined values from array
Create a request timer for measuring duration
Curried filter
Curried flatMap
Await all queued work. Call before returning the response.
Format an amount in minor units (pence/cents) as a currency string. e.g. formatCurrency(1050) → "£10.50" (when currency is GBP)
Format a UTC ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
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.
Format an error context into a human-readable activity log message
Format an error detail string with request context and error message
Generate a random slug with at least 2 digits and 2 letters. Uses Fisher-Yates shuffle on the fixed positions to avoid bias.
Generate a unique slug by retrying until one is not taken.
Collect stats from all registered caches
Get the number of decimal places for a currency code
Get the current request ID, or empty string if outside request context
True when running inside a runWithPendingWork scope (i.e. a request).
Check if a naive datetime-local string is a parseable datetime. Does not interpret timezone — purely a format check.
Validate that a string is a valid IANA timezone identifier.
Resettable lazy reference - like once() but can be reset for testing. Returns [get, set] tuple where set(null) resets to uncomputed state.
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.
Log a debug message with category prefix For detailed debugging during development
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.
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).
Log a completed request to console.debug Path is automatically redacted for privacy
Curried map
Map over a promise-returning function in parallel (Promise.all)
Strip non-numeric characters from a phone number and normalize to +{prefix}{local}
Normalize a user-provided slug: trim, lowercase, replace spaces with hyphens
Current time as a Date
Full ISO-8601 timestamp for created/logged_at fields
Epoch milliseconds for numeric comparisons
Lazy evaluation - compute once on first call, cache forever.
Use instead of let x = null; const getX = () => x ??= compute();
Compose functions left-to-right (pipe) Uses recursive conditional types for arbitrary-length type safety.
Redact dynamic segments from paths for privacy-safe logging Replaces:
Curried reduce
Register a cache stat provider (called at module load time)
Render markdown to HTML (block-level: paragraphs, lists, etc.). Raw HTML is escaped.
Reset the registry (for testing)
Run a function within a pending-work scope
Run a function with a request-scoped random ID for log correlation
Set module-level debug log suppression (avoids env race in parallel tests).
Set module-level request log suppression (avoids env race in parallel tests).
Non-mutating sort with comparator
Get today's date as YYYY-MM-DD in the given timezone.
Convert minor units to major units string for form display. e.g. toMajorUnits(1050) → "10.50" (for GBP)
Convert major units (decimal) to minor units (integer). e.g. toMinorUnits(10.50) → 1050 (for GBP)
Create a TTL (Time-To-Live) cache. Entries expire after ttlMs milliseconds. Accepts an optional clock function for testing.
Remove duplicate values (by reference/value equality)
Remove duplicates by a key function
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.
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.
Validate a normalized slug. Returns error message or null.
A single cache's stats snapshot
Collection cache returned by collectionCache()
Error log context (privacy-safe metadata only)
-
attendeeId: number
Optional: attendee ID
-
code: ErrorCodeType
Error code for classification
-
detail: string
Optional: additional safe context
-
eventId: number
Optional: event ID (not slug)
| { ok: false; error: string; }
Result type for price validation
Slug-with-index pair
Default timezone when none is configured
Error code strings for use in logError calls
Human-readable labels for error codes (shown in admin activity log)
Join an array of strings into a single string (curried reduce shorthand). Replaces the common pattern: reduce((acc: string, s: string) => acc + s, "")
Embeddable widget: iframe integration and CDN storage.
Append iframe=true query param to a URL when in iframe mode
Build embed snippets (script and iframe variants) for a ticket URL
Build a frame-ancestors CSP value from allowed embed hosts. Returns null if the list is empty (allow embedding from anywhere).
Build the full subdomain record name (user choice + suffix). e.g. "myevent" + ".tickets" → "myevent.tickets"
Delete all storage files (images and attachments) for a list of events
Delete the image and attachment files for a single event
Delete a file, routing to local or Bunny based on config.
Upload and publish new script code to Bunny CDN.
Detect iframe mode from a request URL and store it for the current request
Detect the actual image type from magic bytes. Returns the MIME type if matched, null otherwise.
Download and decrypt a file. Returns the decrypted bytes, or null if the file does not exist.
Download raw bytes from storage. Returns null if the file does not exist.
Generate a random CDN filename preserving the original name for readability
Generate a random filename with the correct extension
Get CDN hostname (delegates to bunnyCdnApi for testability).
Get the current request's iframe mode
Get the proxy URL path for serving a decrypted image. Images are encrypted on CDN, so they must be served through the proxy.
Get the MIME type for an image filename from its extension.
Returns which storage backend is active: "bunny", "local", or "none".
Check if image storage is enabled (Bunny CDN or local filesystem).
List files in storage matching a prefix
Parse a comma-separated list of hosts into trimmed, lowercased entries. Filters out empty strings from trailing commas etc.
Register a bunny subdomain (DNS + CDN).
Run fn with an isolated storage configuration (test-only).
Try to delete a file from storage, logging errors on failure
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.
Upload an image to Bunny storage. Encrypts the image bytes before uploading. Returns the filename (without path) on success.
Upload raw bytes to storage, routing to local or Bunny based on config
Validate an attachment file: check size only (any file type allowed).
Validate a custom domain (delegates to bunnyCdnApi for testability).
Validate a comma-separated list of host patterns. Returns null if all valid, or the first error message.
Validate a single host pattern Returns null if valid, or an error message if invalid
Validate an image file: check MIME type, size, and magic bytes.
Attachment validation error
| { valid: false; error: AttachmentValidationError; }
Attachment validation result
Image validation error
User-facing messages for attachment validation errors
Matches a valid hostname like "example.com" or "sub.example.com"
User-facing messages for image validation errors
Maximum attachment file size in bytes (default: 25MB)
Webhook delivery and API examples.
Build a consolidated webhook payload from registration entries
Log attendee registration and send consolidated webhook Used for single-event registrations
Send consolidated webhook to all unique webhook URLs for the given entries
Send a webhook payload to a URL Fires and forgets - errors are logged but don't block registration
Registration entry: event + attendee pair
& { id: number; quantity: number; payment_id: string; price_paid: string; ticket_token: string; date: string | null; }
Attendee data needed for webhook notifications
Example availability response JSON
Example free booking response JSON
Example paid booking response JSON
Example booking request body
Example event matching the webhook example data
The example PublicEvent, produced by toPublicEvent
Example list response JSON
Example single-event response JSON
Example inputs used by both the fixture and the test
Pretty-printed JSON for embedding in documentation
The example payload, matching what buildWebhookPayload would produce
Demo mode and seed data generation.
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.
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.
Check if demo mode is enabled
Pick a random element from an array
Reset cached demo mode value (for testing and cache invalidation)
Explicitly set demo mode on or off (for testing). Bypasses Deno.env to avoid races between parallel test workers.
Wrap a named resource so create/update apply demo overrides to the form
Maps form field names to arrays of possible demo values
Result of a seed operation
Attendee PII fields
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.
Demo event locations — pretend rock-venue / festival listings. Procedurally generated from the venue word pools using a seeded PRNG.
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.
Event metadata fields
Group name and description fields
Holiday name field
Max attendees per seeded event
Site contact page fields
Site homepage fields
Terms and conditions field