# Building BoxOS apps

This document is the terminal-agent reference for the BoxOS 0.5.0 server in this
repository. The server has no package dependencies and runs with Bun. Server configuration
uses `BOXOS_PORT`, `BOXOS_HOST`, `BOXOS_DATABASE`, `BOXOS_BLOBS`,
`BOXOS_WORKERS`, and `BOXOS_EFFECT_CONCURRENCY`. Fuel policy defaults can be
overridden with `BOXOS_INITIAL_FUEL`, `BOXOS_TOP_UP_FUEL`, and
`BOXOS_TOP_UP_INTERVAL_MS`; use high, frequently replenished values only for
trusted local development workloads such as continuous game loops.

## Inspect BoxOS

```sh
curl https://boxos.org/health
curl https://boxos.org/v1/startup
```

BoxOS hosts a dependency-free Node.js/Bun CLI for developers and agents:

```sh
curl -fsSL https://boxos.org/boxos-cli.js -o boxos
chmod +x boxos
./boxos account create
./boxos dev create ./boxos-app
./boxos page publish ./index.html
```

The CLI source is `src/cli/main.ts`; `bun scripts/build_cli.ts` generates the
committed `public/boxos-cli.js` artifact. The CLI emits one JSON value on stdout,
writes errors to stderr, and stores its Ed25519 key at
`~/.boxos/account.json` by default. Use `--key`, `--url`,
`BOXOS_KEY`, and `BOXOS_URL` to override those defaults. Run `./boxos --help`
for publishing, invocation, transfer, messaging, and public-read commands.

`box publish` and `page publish` resolve explicit local box links before
publishing. Use a path relative to the file containing the link:

```text
{{BOXOS_BOX:./counter.box.json}}
```

Links may appear in page HTML or inside a box definition's method strings. Page
HTML may also link a local text file or supported image as an immutable blob:

```text
<img src="/v1/blobs/{{BOXOS_BLOB:./images/logo.png}}" alt="Logo">
```

Images can also be published and downloaded directly:

```sh
./boxos blob publish ./photo.webp
./boxos blob get <blob-id> --output ./photo.webp
```

The CLI resolves the complete graph, calculates its content IDs, and validates every
linked definition locally with the same parser used by the server before it
sends any publication. It then publishes boxes in dependency order and
substitutes their immutable IDs. Repeated paths are deduplicated and dependency
cycles are rejected. A parser rejection is reported on stderr with the local box
path, method, and source location; a failed preflight publishes nothing. The
server validates every definition again as the security boundary.

The examples in `examples/startup/` are deployed idempotently at startup. The
startup endpoint returns their current content-addressed IDs. Never hard-code an
ID from an old run when you can read this endpoint.

Useful source files:

```text
examples/startup/              Example definitions deployed at startup
examples/startup/pages/*.html  Ordinary immutable HTML page sources
examples/startup/boxes/*.ts    Box method definitions
public/client.js               Reference browser client
public/boxos-cli.js            Generated standalone CLI
public/developers.html         Human-facing developer documentation
src/cli/main.ts                TypeScript CLI source
src/server/server.ts           HTTP API
src/server/service.ts          Signing protocol
src/execution/native.ts        ctx API
src/language/parser.ts         Accepted JavaScript subset
src/operations/operations.ts   Shared client/box operations
```

## The model

- An **account** is a raw Ed25519 public key. Its lowercase hexadecimal encoding
  is the 64-character account ID. The private key stays with the client.
- A **blob** is immutable content addressed by its SHA-256 hash. Blob bytes live
  in the server's content-addressed file store; SQLite retains only metadata and
  references. Signed raw publication supports validated PNG, JPEG, WebP, GIF,
  and AVIF images; JSON operations and box publication continue to accept UTF-8 text.
- A **box** is an immutable set of validated JavaScript method bodies plus its
  own public and private key/value storage.
- A **page** is an immutable HTML blob with a shortened 16-character ID.
- A **client ID** is currently the page account ID.
- Pure BoxOS values are `null`, booleans, finite numbers, strings, arrays, and
  plain string-keyed objects. `undefined`, functions, binary values, cycles,
  sparse arrays, accessors, and class instances are not values.

Anyone may read immutable entities and public box storage. Only the box's own
methods can write either public or private box storage. Private storage is only
available while that box executes.

## Page files

Pages should be `.html` files. A minimal page is:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Example</title>
</head>
<body>
  <button id="run">Run</button>
  <script type="module">
    import { boxos } from "/client.js";
    document.querySelector("#run").onclick = async () => {
      const result = await boxos.invoke("BOX_ID", "method", { value: 1 });
      if (!result.ok) throw new Error(result.error);
      console.log(result.value);
    };
  </script>
</body>
</html>
```

Pages are available at:

```text
https://<page-id>.boxos.org/
https://boxos.org/v1/pages/<page-id>
```

Each page subdomain is a separate browser origin and receives a separate page
account in IndexedDB. Link the startup `default.css` blob for the standard
system-aware, dark-first BoxOS design variables and components.

## Single-file developer apps

The CLI can scaffold a dependency-free app that runs directly with Bun:

```sh
./boxos dev create ./todo-app
cd ./todo-app
bun app.js
```

The generated directory contains `boxos-dev.js`, `app.js`, and `README.md`.
`boxos-dev.js` is downloaded from the configured BoxOS server. It exports
`box`, `page`, `startup`, and `publish`. A single source file can define its
boxes and generated page:

```js
import { box, page, publish } from "./boxos-dev.js";

const echo = box({
  run: function run(ctx, input) {
    return input;
  }
}, { name: "echo" });

const app = page({
  title: "Echo",
  boxes: { echo },
  init: async function init(app) {
    const result = await app.boxos.invoke(app.boxes.echo, "run", "hello");
    app.root.textContent = result.value;
  }
});

await publish(echo, app);
```

Box methods must be named function expressions with parameters `(ctx, input)`.
A box with declared dependencies uses `(ctx, input, deps)`. Their trusted
`Function.prototype.toString` source is parsed locally, free references are
rejected, and the extracted bodies are sent as ordinary canonical definitions
for server validation. `startup("accounts.grants")` resolves a current startup
box without hard-coding its ID.

A page accepts exactly one named `init` function plus optional title, styles,
pure data, boxes, and account settings. Generated pages link the current startup
`default.css` blob before app-specific styles, so BoxOS variables and `.bo-*`
components are available by default; set `defaultStyles: false` to opt out. Page
source is serialized into generated
HTML but deliberately is not parsed as box code: it is ordinary browser
JavaScript, and visiting a published page is the user's trust decision. The
function receives one object containing `root`, `document`, `location`, `crypto`,
`boxos`, resolved `boxes`, `data`, `account`, and `disconnectAccount`.

Use `account: { appName, permissions }` to generate the standard Accounts
redirect, state validation, selected-account persistence, and startup discovery.
Publication reads `~/.boxos/account.json` and honors `BOXOS_KEY` and `BOXOS_URL`.
`bun app.js` prints a concise, colored summary in an interactive terminal and
emits one JSON value when stdout is redirected. Pass `{ output: "pretty" }` or
`{ output: "json" }` as the final `publish` argument to force either format.

## Defining boxes

A box definition is pure JSON whose methods are JavaScript **method bodies**, not
whole function declarations:

```json
{
  "methods": {
    "increment": "let n = ctx.storage.public.get(\"count\") || 0; n = n + 1; ctx.storage.public.set(\"count\", n); return n;"
  }
}
```

A definition may include an optional 16-to-128-character nonce:

```json
{
  "nonce": "550e8400-e29b-41d4-a716-446655440000",
  "methods": { "run": "return input;" }
}
```

The box ID hashes the complete canonical definition. The same methods and nonce
always identify the same box and storage; changing the nonce creates independent
storage without changing the methods. The nonce is not tied to an account and
does not grant ownership. Generate it client-side when distinct storage is
needed, and retain it when retrying the same publication.

Each method receives fixed bindings:

```text
ctx, input, JSON, Math, String, Number
```

Methods run synchronously as validated native JavaScript in a worker. One method
or resumed Task continuation produces one atomic commit plan. A single SQLite
writer applies plans in order; an owning worker may execute later turns against
its speculative local state while earlier plans wait in that queue. Results,
messages, and effects become externally visible only after commit. If a turn
throws, runs out of fuel, or returns neither a pure value nor a durable Task, its local
storage writes and declared effects are discarded.

The safe subset rejects ambient globals, `this`, classes, prototypes,
`constructor`, dynamic evaluation, imports, `new`, arrow functions, native
Promises, async/await, and reflective escapes. `Math.random` is unavailable. Use
ordinary named or anonymous `function` expressions for Task continuations.
Computed indexing has the restricted form `value[Number(expression)]`.

### Method context

```js
ctx.account                         // authenticated originating account
ctx.clientId                        // originating page account, or null
ctx.time                            // durable turn invocation time, Unix milliseconds
ctx.self.methodName(input)          // synchronous same-turn self invocation
ctx.timeout(time, callback)         // durable callback at/after an absolute time
ctx.storage.public.get(key)
ctx.storage.public.set(key, value)
ctx.storage.public.delete(key)
ctx.storage.private.get(key)
ctx.storage.private.set(key, value)
ctx.storage.private.delete(key)
ctx.transfer(receiverAccount, amount) // durable Task
ctx.message(clientId, value)
ctx.invoke(boxId, method, input)       // durable Task
ctx.publish(kind, arguments)           // durable Task
ctx.request(request)                    // durable Task
```

`transfer` declares a durable payment Task. Its atomic debit and credit happen
after the declaring turn commits; insufficient fuel rejects the Task. Ignoring
the Task makes the payment fire-and-forget. `message` returns a message ID and
is accepted in the current turn, but delivery happens only after that turn commits.
An unavailable or broken client never rolls back the turn. An HTTP invocation
that emitted messages includes `deliveries: [{ id, clientId, delivered }]` in
its result; `delivered` means at least one live event stream accepted the
message, not that a human read it.

`ctx.time` is assigned when a turn is durably queued and remains fixed across
retries. Synchronous `ctx.self.methodName(input)` calls share the outer turn's
context, storage overlay, fuel reservation, effects, and commit boundary. Their
errors propagate to the caller, and self-call depth is limited to 64.

`ctx.timeout(time, callback)` records a callback atomically with the current turn.
`time` is an absolute non-negative safe-integer Unix timestamp in milliseconds.
At or after that time the callback runs as a fresh fuel-metered turn on the same
box, under the originating account and client ID. It receives the requested time
as its first argument, has a fresh `ctx.time` recording when its callback turn was
queued, cannot capture locals, and may use `ctx.self` to enter a named method in
that same callback turn. Ignoring the returned timeout ID does not cancel it.

`invoke`, `publish`, `request`, and `transfer` return frozen runtime-owned durable Tasks, not
native Promises. A method or continuation may return a pure value for immediate
completion or a Task for eventual completion. Returning a Task makes the current
invocation adopt its outcome. Tasks support:

```js
task.then(successCallback, callbackContext)
task.catch(failureCallback, callbackContext)
```

Both return another Task. Continuations run later as fresh atomic turns on their
origin box. They may return a pure value or another Task; throwing rejects the
next Task. Tasks cannot be stored, messaged, or passed as box input. There is no
`.finally`, native Promise, `async`, or `await`.

Continuation source is captured with the trusted
`Function.prototype.toString.call(callback)`, parsed with the method parser, and
persisted. A continuation cannot capture method locals. Put everything it needs
in explicit callback context:

```js
return ctx.invoke(input.target, "read", input.query).then(
  function completed(result, saved) {
    ctx.storage.private.set(saved.key, result);
    return result;
  },
  { key: input.key }
);
```

A minimal durable loop can schedule a standalone callback that enters a method
synchronously:

```js
let next = ctx.time + 100;
ctx.storage.private.set("next", next);
ctx.timeout(next, function wake(scheduledTime) {
  return ctx.self.tick({ scheduledTime: scheduledTime });
});
```

The `tick` method should compare `scheduledTime` with private state to make stale
or duplicate timers harmless. All box methods remain public invocation entry
points, including methods called through `ctx.self`, so each method must enforce
its own authorization and invariants.

This is invalid because `key` is a free variable:

```js
let key = input.key;
return ctx.invoke(input.target, "read", null).then(
  function completed(result) {
    ctx.storage.private.set(key, result);
    return result;
  }
);
```

### Publishing from a box

```js
return ctx.publish("blob", {
  text: "...",
  contentType: "text/plain"
}).then(function published(result) {
  return result.id;
});

ctx.publish("page", { blobId: "..." });
ctx.publish("box", { methods: { run: "return input;" } });
ctx.publish("account", { pubkey: "..." });
```

A successful publication Task settles with `{ id }`. An effect is still durable
when its Task is not returned or observed, allowing explicit fire-and-forget
publication.

### Public HTTPS requests from a box

`ctx.request` declares a durable, non-streaming HTTPS request. It is deliberately
not raw `fetch`: the structure admits only public HTTPS JSON API requests.

```js
return ctx.request({
  host: "api.example.com",
  path: "/v1/messages?format=json",
  method: "POST",
  timeoutMs: 120000,
  headers: { Authorization: "Bearer " + input.token },
  body: { message: input.message }
}).then(function completed(response, saved) {
  if (response.ok) ctx.storage.private.set(saved.key, response.body);
  else ctx.message(saved.clientId, { error: response.error || response.body });
  return response;
}, { key: "last-response", clientId: ctx.clientId });
```

The API accepts only:

- a multi-label public DNS `host`, without a scheme, port, user info, or IP literal;
- an absolute `path` of at most 4096 characters;
- `GET` without a body or `POST` with an optional pure-value JSON body;
- an optional `timeoutMs` from 1000 to 120000 milliseconds, defaulting to 30000;
- ordinary end-to-end string headers. Transport headers such as `Host`,
  `Content-Length`, and `Transfer-Encoding` are runtime-owned.

Requests always use HTTPS on port 443, pin a publicly routable DNS result for
the TLS connection, validate the certificate for `host`, and never follow
redirects. Private, loopback, link-local, mixed public/private DNS, and other
non-public destinations are rejected. Request and response bodies are limited
to 256 KiB.

The request Task settles with
`{ ok, requestId, durationMs, status, contentType, body }` for an HTTP response or
`{ ok: false, requestId, durationMs, error }` for a transport failure. `durationMs`
measures effect execution after dispatch, including validation and network time but
not time waiting in the effect queue. JSON responses become
pure BoxOS values; other response bodies are strings. As with all external POST
requests, a crash after remote acceptance but before durable settlement can
cause a retry. Use an upstream idempotency key when available.

## Signing terminal requests

HTTP mutations use Ed25519 signatures. Encode public keys and signatures as
lowercase hexadecimal. Canonical JSON recursively sorts object keys while
preserving array order.

```ts
function canonical(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(canonical)
  if (value !== null && typeof value == "object") {
    const record = value as Record<string, unknown>
    return Object.fromEntries(Object.keys(record).sort().map(key => [key, canonical(record[key])]))
  }
  return value
}

function hex(bytes: ArrayBuffer): string {
  return [...new Uint8Array(bytes)].map(byte => byte.toString(16).padStart(2, "0")).join("")
}

const keys = await crypto.subtle.generateKey(
  { name: "Ed25519" }, true, ["sign", "verify"],
) as CryptoKeyPair
const account = hex(await crypto.subtle.exportKey("raw", keys.publicKey))

async function signed(purpose: string, request: unknown) {
  const message = `${purpose}\n${JSON.stringify(canonical(request))}`
  const signature = hex(await crypto.subtle.sign(
    { name: "Ed25519" }, keys.privateKey, new TextEncoder().encode(message),
  ))
  return { account, signature, request }
}
```

A valid unknown account is registered automatically on its first signed
interaction. It receives initial fuel and is lazily topped up when it interacts
after the configured interval. Exact signed-request replay is idempotent.

Use a fresh `crypto.randomUUID()` nonce for each new action. Reuse the complete
same signed request only when retrying that action.

## HTTP API

All request and response bodies below are JSON unless stated otherwise.

### Public reads

```text
GET /health
GET /AGENTS.md
GET /developers
GET /client.js
GET /boxos-cli.js
GET /boxos-dev.js
GET /boxos
GET /open/<capability>?data=<encoded-opaque-string>
GET /v1/startup
GET /v1/boxes/<64-char-box-id>
GET /v1/boxes/<box-id>/storage/public?key=<encoded-key>
GET /v1/blobs/<64-char-blob-id>
GET /v1/pages/<16-char-page-id>
```

`GET /v1/startup` returns:

```json
{
  "deployments": {
    "accounts.page": { "kind": "page", "id": "..." },
    "accounts.grants": { "kind": "box", "id": "..." },
    "accounts.pictures": { "kind": "box", "id": "..." },
    "accounts.profiles": { "kind": "box", "id": "..." },
    "app-explorer.catalog": { "kind": "box", "id": "..." },
    "app-explorer.metadata": { "kind": "box", "id": "..." },
    "app-explorer.installs": { "kind": "box", "id": "..." },
    "app-explorer.handlers": { "kind": "box", "id": "..." },
    "app-explorer.open-preferences": { "kind": "box", "id": "..." },
    "app-explorer.page": { "kind": "page", "id": "..." },
    "profile.page": { "kind": "page", "id": "..." },
    "social.messages": { "kind": "box", "id": "..." },
    "social.page": { "kind": "page", "id": "..." },
    "shuter.game": { "kind": "box", "id": "..." },
    "shuter.page": { "kind": "page", "id": "..." },
    "default.css": { "kind": "blob", "id": "..." }
  }
}
```

A public storage response is either:

```json
{ "found": true, "value": "..." }
```

or:

```json
{ "found": false }
```

### Publish image bytes

Images use a raw signed request so binary bytes never need base64 or the BoxOS
value model. Sign with purpose `boxos.publish-blob-bytes.v1`:

```http
POST /v1/blobs
Content-Type: image/png
X-BoxOS-Account: <account>
X-BoxOS-Nonce: <uuid>
X-BoxOS-SHA256: <sha256-of-body>
X-BoxOS-Size: <decimal-byte-count>
X-BoxOS-Signature: <signature>

<raw image bytes>
```

The signature covers the canonical request:

```json
{
  "contentType": "image/png",
  "nonce": "<uuid>",
  "sha256": "<sha256-of-body>",
  "size": 12345
}
```

The server verifies the signed digest and size, image encoding, and dimensions.
PNG, JPEG, WebP, GIF, and AVIF are supported up to 5 MiB and 8192 by 8192 pixels.
SVG is not accepted by this endpoint. Response:

```json
{
  "id": "<blob-id>",
  "contentType": "image/png",
  "size": 12345,
  "width": 640,
  "height": 480,
  "url": "/v1/blobs/<blob-id>"
}
```

`GET` and `HEAD /v1/blobs/<blob-id>` return the original bytes publicly with
immutable caching, their recorded content type, content length, and hash ETag.

### Publish a box

Sign with purpose `boxos.publish-box.v1`:

```http
POST /v1/boxes
```

```json
{
  "account": "<public-key>",
  "signature": "<signature>",
  "request": {
    "nonce": "<uuid>",
    "definition": { "methods": { "run": "return input;" } }
  }
}
```

Response: `{ "id": "<box-id>" }`.

### Invoke a box

Sign with purpose `boxos.invoke.v1`:

```http
POST /v1/invoke
```

```json
{
  "account": "<public-key>",
  "signature": "<signature>",
  "request": {
    "nonce": "<uuid>",
    "boxId": "<box-id>",
    "method": "run",
    "input": null,
    "clientId": null,
    "max_fuel": 10000
  }
}
```

A browser page sets `clientId` to its account. A terminal may use `null`.
`max_fuel` is optional. Without it, the server reserves up to 10000 from the
available balance. With it, the server requires and reserves that exact amount.
The server charges deterministic transpiled execution and runtime data costs,
then refunds the unused amount. Each cross-box method and continuation
makes its own reservation from the originating account.
Response:

```json
{ "ok": true, "value": null }
```

or:

```json
{ "ok": false, "error": "..." }
```

If the method returns a durable Task, this is the outcome of the complete Task
chain rather than only the initial synchronous turn. Disconnecting does not
cancel it. Retrying the exact signed request observes the same idempotent
invocation instead of starting another one.

Signatures cover only the nested `request`, not the outer envelope.

### Direct operations

Sign with purpose `boxos.operation.v1` and send:

```http
POST /v1/operations
```

```json
{
  "account": "<public-key>",
  "signature": "<signature>",
  "request": {
    "nonce": "<uuid>",
    "operation": { "type": "..." }
  }
}
```

Supported operations:

```json
{ "type": "transfer", "receiver": "<account>", "amount": 100 }
{ "type": "message", "clientId": "<account>", "message": { "hello": true } }
{ "type": "publishBlob", "text": "...", "contentType": "text/html; charset=utf-8" }
{ "type": "publishPage", "blobId": "<blob-id>" }
```

A direct `message` operation returns `{ id, delivered }`. It commits acceptance
before attempting best-effort delivery, so `delivered: false` is a successful
operation when the client is offline.

The CLI performs this sequence for `page publish`: it transitively publishes
`BOXOS_BOX` dependencies and `BOXOS_BLOB` files, substitutes their IDs, publishes
the linked HTML blob, publishes the page, and prints its subdomain URL. A client
using direct HTTP operations performs those same steps explicitly.

### Client events

Messages use authenticated Server-Sent Events over a streaming POST, not a
WebSocket and not the browser `EventSource` constructor. Delivery is transient
and best effort: messages are not queued for offline clients, and delivery
failure cannot abort the committed box turn or direct operation.

Sign with purpose `boxos.events.v1`:

```http
POST /v1/events
Accept: text/event-stream
```

```json
{
  "account": "<page-account>",
  "signature": "<signature>",
  "request": { "nonce": "<uuid>", "clientId": "<same-page-account>" }
}
```

`boxos.events(handler, { signal })` opens this POST stream and stops when the
signal is aborted or the connection ends. It does not automatically reconnect or
replay missed events; an application that wants reconnection must call it again
with a fresh subscription nonce. The stream emits `message` events whose data is:

```json
{ "id": "...", "sender": "<account>", "message": "<pure-value>" }
```

## Browser client

`/client.js` exports `boxos`:

```js
import { boxos } from "/client.js";

await boxos.account();
await boxos.invoke(boxId, method, input, { maxFuel: 10000 });
await boxos.publishBox(definition);
await boxos.publishBlob(text, contentType);
await boxos.publishFile(file, { signal });
await boxos.publishBytes(bytes, "image/png", { signal });
await boxos.readBlob(blobId, { signal });
boxos.blobUrl(blobId);
await boxos.publishPage(blobId);
await boxos.transfer(receiver, amount);
await boxos.message(clientId, value);
await boxos.readPublic(boxId, key);
await boxos.events(message => console.log(message), { signal });
```

The client creates one non-extractable page-account key in origin-scoped
IndexedDB. Human identity accounts managed by the Accounts example are distinct
from this automatic page account.

## Authentication and the reference Accounts flow

BoxOS itself authenticates **accounts**, not people: an account is controlled by
an Ed25519 private key, and every mutating request is authorized by its
signature. A page automatically receives its own origin-scoped page account
from `/client.js`; this is often enough for an app that only needs to identify
its installation or authorize its own boxes.

The startup Accounts app provides one useful, optional convention for apps that
need authority from a human account. It is a reference implementation, not an
enforced application structure. An app may instead use its own account picker,
its own box-defined login flow, terminal keys, guest access, or another
application-specific identity model. The protocol only requires signed
accounts; authorization policy belongs in the target box.

### Optional human-account capability flow

Use this flow when an app wants a human to approve named capabilities:

1. Read `/v1/startup` and discover `accounts.page`, `accounts.grants`, and any
   other startup dependencies. Do not hard-code their IDs.
2. Redirect the browser to the discovered Accounts page with:

   ```text
   app_name=<displayed app name>
   app_account=<requesting page account>
   permissions=<comma-separated capabilities>
   redirect_uri=<HTTP(S) return URL>
   state=<unguessable state>
   ```
3. On return, validate the URL fragment and verify that `state` exactly matches
   the value issued by the app. The fragment contains either
   `error=access_denied`, or:

   ```text
   account=<selected human account>
   state=<original state>
   grants_box=<box ID>
   profiles_box=<box ID>
   ```
4. Treat the returned human account as the account whose authority was
   delegated; the app still signs requests with its page account. Pass the
   selected account and the relevant capability to a box method, which must
   perform the durable grant check.

In the startup reference implementation, a grant is public storage in the
grants box under:

```text
<owner-account>|<grantee-page-account>|<permission>
```

That key format, the grants box, and the permission names are conventions of
the example—not BoxOS-wide requirements. Do not assume that a page has a human
account, that a grant exists, or that a particular capability is available;
handle denial and unavailable startup deployments explicitly.

The startup Profiles box stores public profile names. The separate Pictures box
stores public image blob IDs under `image|<account>` so profile-name storage keeps
its existing immutable box identity. In that example, an app granted `manage
account` can call:

```json
{
  "method": "setName",
  "input": {
    "account": "<human-account>",
    "name": "New name"
  }
}
```

The box durably checks the grant and returns `{ "name": "New name" }`. Read the
current name from `name|<human-account>`. The Pictures box similarly provides
`setImage` with `{ account, blobId }` and `removeImage` with `{ account }`; both
check `manage account`. Read the current blob ID from `image|<human-account>`.
The Accounts app sets the initial name
during creation but does not expose profile renaming.

## Repository startup examples

A startup page is an HTML template with uppercase placeholders:

```html
<script type="module">
  const boxId = "{{MY_BOX}}";
</script>
```

`examples/startup/deploy.ts` publishes dependencies in order, replaces the
placeholders, publishes the HTML blob, publishes the page, and records a friendly
name in `startup_deployments`. Keep page behavior in `.html`; do not generate
whole pages from TypeScript.

Before finishing an app change, run:

```sh
bunx tsc --noEmit
bun test
git diff --check
```

For generated or templated page scripts, add a test that extracts and parses the
module source. This catches browser syntax errors before deployment.
