Menu

microhost.d.ts

Editor declarations for the SDK. Download microhost.d.ts.

// MicroHost JavaScript SDK, API version 1 (SDK_API_VERSION = 1 in crates/engine-api).
//
// Ambient declarations for site code. Anything not declared here is not part of the SDK:
// engines do not provide it, and conformance scenarios check that it stays absent.
// See sdk/README.md for the supported feature set and the explicit exclusions.
//
// Every host call (database, mail, storage, env, fetch) returns a Promise. Failures are thrown
// with a stable `code` property; messages are generic and carry no paths or SQL text.

/** Header input accepted by Headers, Response and fetch. Names are matched case-insensitively. */
type HeadersInit = Headers | Record<string, string> | [string, string][];

/** Body input accepted by Response and fetch. */
type BodyInit = string | Uint8Array | null;

/** A SQL parameter. Mapping is defined in sdk/README.md (section "ctx.db"). */
type SqlParam =
  | null
  | undefined
  | boolean
  | number
  | bigint
  | string
  | Uint8Array
  | ArrayBuffer;

/** Error thrown by host calls. `code` is stable; `message` is generic. */
interface MicroHostError extends Error {
  readonly code:
    | "capability_denied"
    | "CAPABILITY_DENIED"
    | "QUOTA_EXCEEDED"
    | "INVALID_INPUT"
    | "HOST_UNAVAILABLE"
    | "HOST_INTERNAL"
    | "invalid_admin_request"
    // Lowercase codes raised by the SDK modules (microhost/auth, /db, /session, /sql).
    | "invalid_input"
    | "quota_exceeded"
    | "host_unavailable"
    | "db_use_sql_template"
    | "db_multiple_rows"
    | "db_duplicate_column"
    | "db_no_such_column"
    | "db_where_required"
    | "db_invalid_direction"
    | "sdk_module_disabled";
}

/** Returned only after host verification of a private admin invocation. */
interface VerifiedAdminRequest {
  readonly userId: string;
  readonly role: "viewer" | "editor" | "developer" | "owner";
  readonly workspace: string;
  readonly site: string;
  readonly extension: string;
  readonly name: string;
  readonly operation: string;
  readonly issuedAt: number;
}

/** A safe error with code and message invalid_admin_request. */
interface AdminRequestError extends Error {
  readonly code: "invalid_admin_request";
}

/** Verifies the eight signed admin headers through the host; signing keys never reach JavaScript. */
declare function verifyAdminRequest(request: Request, ctx: SiteContext): VerifiedAdminRequest;

/** Request headers have lowercase names. Only the methods listed here exist in v1. */
declare class Headers {
  constructor(init?: HeadersInit);
  get(name: string): string | null;
  has(name: string): boolean;
  set(name: string, value: string): void;
  append(name: string, value: string): void;
  delete(name: string): void;
}

/** Absolute https URL. Only the properties listed here exist in v1; URLSearchParams does not. */
declare class URL {
  constructor(input: string, base?: string | URL);
  readonly href: string;
  readonly origin: string;
  readonly protocol: string;
  readonly host: string;
  readonly hostname: string;
  readonly pathname: string;
  readonly search: string;
  readonly hash: string;
  toString(): string;
}

/** The request handed to the handler. The body is fully buffered and size limited. */
declare class Request {
  /** Not constructible in v1; instances are provided to the handler. */
  private constructor();
  /** Uppercase HTTP method. */
  readonly method: string;
  /** Absolute URL, `https://host/path?query`. */
  readonly url: string;
  /** Header names are lowercase. */
  readonly headers: Headers;
  /** Decoded route captures. This is an immutable empty object for exact and convention routes. */
  readonly params: Readonly<Record<string, string>>;
  text(): Promise<string>;
  json(): Promise<unknown>;
  arrayBuffer(): Promise<ArrayBuffer>;
}

interface ResponseInit {
  /** 200 to 599 inclusive. Other values throw a RangeError. */
  status?: number;
  headers?: HeadersInit;
}

/** The response the handler must return. Hop-by-hop headers and content-length are stripped by the host. */
declare class Response {
  constructor(body?: BodyInit, init?: ResponseInit);
  /** Serializes `value` with JSON.stringify and sets content-type: application/json. */
  static json(value: unknown, init?: ResponseInit): Response;
  /** Sets content-type: text/html; charset=utf-8 and x-content-type-options: nosniff. */
  static html(html: string, init?: ResponseInit): Response;
  /** Redirect with a Location header. Only 301, 302, 303, 307 and 308 are accepted. */
  static redirect(url: string | URL, status: 301 | 302 | 303 | 307 | 308): Response;
}

interface SiteDbResult {
  columns: string[];
  rows: unknown[][];
  rowsAffected: number;
  /** Number for safe integers, bigint otherwise, null when not applicable. */
  lastInsertRowid: number | bigint | null;
}

interface SiteDbStatement {
  sql: string;
  params?: SqlParam[];
}

/** Immutable SQL fragment produced by the `microhost/sql` tagged template. */
interface SqlFragment {
  readonly text: string;
  readonly parameters: readonly SqlParam[];
}

/** Build parameterized SQL. Interpolated fragments compose; arrays expand to placeholders. */
declare function sql(strings: TemplateStringsArray, ...values: (SqlParam | SqlFragment | readonly SqlParam[])[]): SqlFragment;

interface DbRows<T extends object = Record<string, unknown>> extends Array<T> {
  readonly hasMore: boolean;
  first(): T | null;
  next(): Promise<DbRows<T>>;
}

type DbFilter<T extends object> = Partial<{ [K in keyof T]: T[K] | readonly T[K][] | null }>;
interface DbQuery<T extends object> {
  orderBy(key: keyof T & string | SqlFragment, direction?: "asc" | "desc"): DbQuery<T>;
  limit(count?: number): DbQuery<T>;
  offset(count: number): DbQuery<T>;
  all(): Promise<DbRows<T>>;
  [Symbol.asyncIterator](): AsyncIterator<T>;
}
interface DbTable<T extends object, K extends keyof T = keyof T> {
  readonly name: string;
  readonly columns: readonly (keyof T & string)[];
  readonly primaryKey: K | null;
  get(key: T[K]): Promise<T | null>;
  find(filter?: DbFilter<T>): Promise<T | null>;
  where(filter?: DbFilter<T>): DbQuery<T>;
  count(filter?: DbFilter<T>): Promise<number | bigint>;
  insert(values: Partial<T>): Promise<T | null>;
  update(keyOrFilter: T[K] | DbFilter<T>, values: Partial<T>): Promise<T[]>;
  delete(keyOrFilter: T[K] | DbFilter<T>): Promise<T[]>;
  stmt: {
    insert(values: Partial<T>): SqlFragment;
    insertOrIgnore(values: Partial<T>): SqlFragment;
  };
}
interface MicroHostDbModule {
  all<T extends object = Record<string, unknown>>(statement: SqlFragment): Promise<T[]>;
  one<T extends object = Record<string, unknown>>(statement: SqlFragment): Promise<T | null>;
  run(statement: SqlFragment): Promise<{ rowsAffected: number; lastInsertRowid: number | bigint | null }>;
  tx(statements: readonly SqlFragment[]): Promise<SiteDbResult[]>;
  id(): string;
  now(): string;
}

/** Present on ctx only when the site was granted database access. */
interface SiteDb {
  query(sql: string, params?: SqlParam[]): Promise<SiteDbResult>;
  execute(sql: string, params?: SqlParam[]): Promise<SiteDbResult>;
  /** All-or-nothing. Rejects on the first failing statement after rollback. No interactive transactions in v1. */
  transaction(statements: SiteDbStatement[]): Promise<SiteDbResult[]>;
}

/** A named workspace database handle. Same shapes as `ctx.db`; governed by a per-database grant. */
interface SiteWorkspaceDb {
  query(sql: string, params?: SqlParam[]): Promise<SiteDbResult>;
  execute(sql: string, params?: SqlParam[]): Promise<SiteDbResult>;
  transaction(statements: SiteDbStatement[]): Promise<SiteDbResult[]>;
}

/** Present on ctx only when the release requested workspace databases and a grant exists. */
interface SiteWorkspaceDbs {
  /** `name` is 1 to 64 letters, digits, underscores or hyphens; anything else throws INVALID_INPUT. */
  get(name: string): SiteWorkspaceDb;
}

interface SiteMailMessage {
  to: string | string[];
  subject: string;
  text: string;
  html?: string;
}

/** Present on ctx only when the site was granted mail. The sender is the site identity, not chosen by tenant code. */
interface SiteMail {
  /** Resolves when accepted for delivery; it does not mean the provider has delivered it. */
  send(message: SiteMailMessage): Promise<{ messageId: string }>;
}

/** Present on ctx only when the site was granted storage. Keys are safe relative paths. */
interface SiteStorage {
  get(key: string): Promise<Uint8Array | null>;
  /** Strings are stored as UTF-8. */
  put(key: string, value: string | Uint8Array): Promise<void>;
  /**
   * Atomically appends to the object, creating it when missing. Concurrent appends never lose or
   * interleave bytes inside one call. The object may not grow past 16 MiB and counts against the storage quota.
   */
  append(key: string, value: string | Uint8Array): Promise<void>;
  delete(key: string): Promise<boolean>;
  /**
   * Returns a response that makes the platform stream the object to the visitor, with `Range`,
   * `ETag` and `304` handled for you. Your code decides who may download (check the session, then
   * call this); the bytes never pass through your handler, so an object of any size up to the
   * store limit works. `init.headers` such as `Content-Type`, `Content-Disposition` and
   * `Cache-Control` are kept; framing and validators come from the object. A missing object answers
   * 404. Only the site's own objects can be named.
   */
  sendFile(key: string, init?: { headers?: HeadersInit }): Response;
  /** Sorted names under `prefix`, at most `limit` (default and maximum 1000). No cursors in v1. */
  list(prefix: string, options?: { limit?: number }): Promise<string[]>;
}

/** Present on ctx only when the site declares jobs and was granted jobs. */
interface SiteJobs {
  /**
   * Queues a run of a job declared in the manifest and resolves with its durable run id. `payload`
   * is any JSON value up to 64 KiB, read by the job with `request.json()`. While a run with the same
   * `key` is queued or running, that run is returned instead of a new one. `delaySeconds` is 0 to
   * 604800. Rejects with `INVALID_INPUT` for an unknown job, `QUOTA_EXCEEDED` when the queue is full.
   */
  enqueue(
    name: string,
    payload?: unknown,
    options?: { delaySeconds?: number; key?: string },
  ): Promise<{ runId: number }>;
}

/** Present on ctx only when the site was granted secrets access. Unknown or reserved names resolve to undefined. */
interface SiteEnv {
  get(name: string): Promise<string | undefined>;
}

/** The context passed to the handler. Optional members are absent (undefined) unless granted. */
interface SiteContext {
  db?: SiteDb;
  dbs?: SiteWorkspaceDbs;
  mail?: SiteMail;
  storage?: SiteStorage;
  jobs?: SiteJobs;
  env?: SiteEnv;
  /** Present only on private /_admin/ invocations. */
  admin?: { verify(request: Request): VerifiedAdminRequest };
}

/** A requested capability was not granted or is unavailable for this execution. */
interface CapabilityUnavailableError extends Error {
  readonly code: "capability_denied";
}

interface FetchInit {
  method?: string;
  headers?: HeadersInit;
  body?: string | Uint8Array;
}

interface FetchResponse {
  readonly status: number;
  readonly ok: boolean;
  readonly headers: Headers;
  text(): Promise<string>;
  json(): Promise<unknown>;
  arrayBuffer(): Promise<ArrayBuffer>;
}

/** Outbound HTTPS through the egress broker. Host, redirects and size are policy-controlled. No AbortSignal, streams or cookies. */
declare function fetch(input: string | URL, init?: FetchInit): Promise<FetchResponse>;

declare class TextEncoder {
  readonly encoding: "utf-8";
  encode(input?: string): Uint8Array;
}

declare class TextDecoder {
  readonly encoding: "utf-8";
  decode(input?: Uint8Array | ArrayBuffer): string;
}

declare const crypto: {
  /** Fills `array` with random bytes. At most 65536 bytes per call. */
  getRandomValues<T extends Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Int32Array>(array: T): T;
};

/** Output is bounded (64 KiB per execution) and goes to the site log, never to the visitor. */
declare const console: {
  log(...args: unknown[]): void;
  info(...args: unknown[]): void;
  warn(...args: unknown[]): void;
  error(...args: unknown[]): void;
};

/**
 * Handler signature. The module's default export must be an async function that resolves to a
 * Response. Any other resolved value is an error (InvalidResponse, HTTP 502).
 *
 * @example
 * export default async function handle(request: Request, ctx: SiteContext): Promise<Response> {
 *   return Response.json({ ok: true });
 * }
 */
type SiteHandler = (request: Request, ctx: SiteContext) => Promise<Response>;

declare module "microhost/sql" {
  export interface SqlFragment { readonly text: string; readonly parameters: readonly SqlParam[]; }
  export function sql(strings: TemplateStringsArray, ...values: (SqlParam | SqlFragment | readonly SqlParam[])[]): SqlFragment;
}
declare module "microhost/db" {
  export const all: MicroHostDbModule["all"];
  export const one: MicroHostDbModule["one"];
  export const run: MicroHostDbModule["run"];
  export const tx: MicroHostDbModule["tx"];
  export const id: MicroHostDbModule["id"];
  export const now: MicroHostDbModule["now"];
}

declare module "microhost/auth" {
  export function hashPassword(password: string): Promise<string>;
  export function verifyPassword(password: string, storedHash: string): Promise<boolean>;
}
declare module "microhost/session" {
  export interface Session { readonly userId: string; readonly data: Readonly<Record<string, unknown>>; }
  export interface SessionOptions { readonly maxAgeSeconds?: number; readonly data?: Record<string, unknown>; }
  export function create(userId: string, options?: SessionOptions): Promise<string>;
  export function get(request: Request): Promise<Session | null>;
  export function destroy(request: Request): Promise<string>;
  export function destroyAll(userId: string): Promise<void>;
}
declare module "db" {
  export * from "microhost/db";
}

// Build-profile declarations. These names are exported by the server-shipped `microhost` module
// while resolving microhost.config.js; they are not available to request handlers.
interface BuildRoute {
  readonly path: string;
  readonly script: string;
  readonly methods: readonly string[];
}
/** Names accepted in the manifest `sdk` list. `db` implies `sql`. */
type BuildSdkModule = "auth" | "db" | "session" | "sql";

/** Value returned by `defineExtension`; passed in the `admin` list of `defineSite`. */
interface BuildExtension {
  readonly [key: string]: unknown;
}
/** Value returned by `field.<type>(...)`, used in extension input and output descriptors. */
interface BuildField {
  readonly [key: string]: unknown;
}
interface BuildFieldOptions {
  /** Author-friendly spelling of the schema's `enum`. */
  readonly values?: readonly string[];
  readonly [key: string]: unknown;
}
type BuildFieldFactory = (options?: BuildFieldOptions) => BuildField;

interface BuildSite {
  readonly schemaVersion: 1;
  readonly schemaCompat?: { readonly min: number; readonly max: number };
  readonly static?: { readonly root?: string; readonly spaFallback?: boolean };
  /**
   * Custom pages for errors the platform produces. Keys: a status from 400 to 599, "4xx", "5xx" or
   * "_" (everything else). A value ending in ".js" is a script that reads the status from
   * `request.params.status`; any other value is a file inside `public/`. The visitor always gets the
   * original status.
   */
  readonly errorPages?: { readonly [statusOrClass: string]: string };
  /**
   * The path the post-activation health check requests (default "/"). It must start with "/", be
   * at most 256 bytes of visible ASCII, contain no "..", "?" or "#", and not start with "/_admin".
   * It may name a static file, a route or a script route; the check needs a 2xx answer.
   */
  readonly healthCheck?: { readonly path: string };
  /**
   * Scheduled and background jobs, by name. A job is a script under `jobs/`; it runs on a cron
   * `schedule` (read in `timezone`, default UTC) or on demand, and needs the jobs capability.
   */
  readonly jobs?: {
    readonly [name: string]: {
      readonly script: string;
      readonly schedule?: string;
      readonly timezone?: string;
      readonly overlap?: "skip" | "queue";
      readonly catchUp?: "none" | "latest";
      readonly timeoutMs?: number;
      readonly retries?: number;
    };
  };
  readonly routes?: readonly BuildRoute[];
  readonly permissionsRequested?: {
    readonly database?: boolean;
    readonly workspaceDatabases?: readonly string[];
    readonly email?: boolean;
    readonly storage?: boolean;
    readonly secrets?: boolean;
    readonly externalHttp?: readonly string[];
  };
  readonly limitsRequested?: { readonly timeoutMs?: number; readonly memoryMb?: number };
  /** SDK modules the release imports. Absent: every module is importable. */
  readonly sdk?: readonly BuildSdkModule[];
  readonly admin?: readonly BuildExtension[] | unknown;
}
declare function defineSite(site?: Omit<BuildSite, "schemaVersion">): BuildSite;
declare function route(path: string, script: string, options?: { methods?: readonly string[] }): BuildRoute;
/** Declares an admin extension. Queries default to GET/viewer/read, actions to POST/editor/write. */
declare function defineExtension(extension: { readonly [key: string]: unknown }): BuildExtension;
declare const field: {
  string: BuildFieldFactory;
  number: BuildFieldFactory;
  status: BuildFieldFactory;
  date: BuildFieldFactory;
  boolean: BuildFieldFactory;
  text: BuildFieldFactory;
  textarea: BuildFieldFactory;
};
declare const files: {
  list(prefix?: string): readonly string[];
  read(path: string): string | Uint8Array;
};