# amvet — build a page against this app

Everything you need is on this origin. Load one module, call `login()`, call ops.

## Load it

```html
<script type="module">
  import { createClient } from "https://amvet.lakeshorelabs.dev/client/client.js";
  const app = createClient();

  // A browser blocks a popup that no click opened, so sign in from a button.
  document.querySelector('#signin').onclick = async () => {
    await app.auth.login();
    const rows = await app.listRecords({ entity: "…" });
    document.querySelector('#out').textContent = JSON.stringify(rows, null, 2);
  };
</script>
```

## Read

```js
const rows = await app.listRecords({ entity: "…" });
```

The value you receive is exactly the declared type — nothing else wraps it. Where a
list operation's declared type has a `result` member, that member IS the shape; there is
no other envelope.

## Write

```js
// This app exposes no write op.
```

Every op, with its exact arguments and result, is in [`client.d.ts`](https://amvet.lakeshorelabs.dev/client/client.d.ts).
This app exposes 3.

## When something fails

A call rejects with a `LakeshoreError`. `code` is a stable machine code — the app's
own failure code, or one of `popup_blocked`, `login_required`, `login_cancelled`,
`login_timeout`, `unauthenticated`, `transport_error`, `op_failed` — and `detail` carries
the app's problem+json body when it sent one.

```js
try {
  await app.listRecords({ entity: "…" });
} catch (err) {
  console.error(err.code, err.message, err.detail);
}
```

## Sign-in, and what it costs you

`login()` opens this app's own login window. The user signs in there (never on your
page — you never see a credential) and approves your page by name, for every operation this
app exposes (the consent page lists them; there is nothing narrower to ask for). Your page then holds
**an access token, in memory, for one hour**. There is no refresh token: nothing durable
about this sign-in is ever stored on your origin, so a reload signs in again.

Once the user has approved your page, signing in again is a flash rather than a decision —
but it still opens a window, and **a browser only opens a window a recent click asked
for**. So keep sign-in on a click. A call whose hour lapsed while nobody was clicking
rejects with `login_required`; show a button, and `login()` from it.

`app.auth.logout()` revokes the token at the app immediately. It does not sign the
user out of the app itself.

## Where you can host this

Anywhere that serves over `https` (or `http://localhost` while you work) — Vercel,
Cloudflare Pages, GitHub Pages, your own server. Two places it will NOT work:

- **inside a Claude or ChatGPT artifact/canvas sandbox** — their content-security policy
  blocks requests to this origin, and no code here can change that. Build the page there
  if you like; run it somewhere else.
- **from a `file://` page** — its origin is `null`, which a browser will not deliver the
  sign-in message to. Serve the file over `http://localhost` instead.

## The rest of the kit

- Types: https://amvet.lakeshorelabs.dev/client/client.d.ts
- Client: https://amvet.lakeshorelabs.dev/client/client.js
- Index: https://amvet.lakeshorelabs.dev/llms.txt
- OpenAPI: https://amvet.lakeshorelabs.dev/api/openapi.yaml (the app's full HTTP surface — a browser session reads it; a
  kit token cannot, which is why the kit calls the ops above instead)
- MCP endpoint: https://amvet.lakeshorelabs.dev/api/v1/mcp (the same ops, for an MCP client)
- OAuth metadata: https://amvet.lakeshorelabs.dev/.well-known/oauth-authorization-server
