# Hosted resolve pages

Let your own end users finish a parked job on a page with no account: type a one-time code, apply a fix, or take over.

A **hosted resolve page** (`https://app.kaab.ist/r/<token>`) lets a person without a Kaabist
account finish one parked job: type the code their bank has texted them, apply a suggested fix,
or deal with the page themselves. You send them the link; the page names your organisation, says
in plain words what is needed, and does one thing for one job.

## When to use it

Use a hosted page when the value or the decision belongs to **your end user**, not to you: the
one-time code arrives on their phone, or the account the job signs in to is theirs. The code then
goes from their phone to the job without passing through your servers.

When you, your backend or your operators can supply the value, answer through the API instead
(`POST /v1/jobs/{id}/input`, or an SDK's `onInput`); see
[Authenticated flows](https://docs.kaab.ist/guides/authenticated-flows). When a stopped job needs a judgement about the
program itself (skipping a step, retrying from a checkpoint), handle it in the
[panel](https://docs.kaab.ist/panel/takeover): a hosted page never offers those.

## Collecting a value

Declare the input as hosted in the program:

```json
{ "await_input": { "name": "otp", "prompt": "The code we sent you", "pattern": "^[0-9]{6}$",
  "collect": "hosted", "hosted": { "notify": "none" } } }
```

When the job parks there, its envelope carries the page's address from the first moment:

```json
"input_request": { "name": "otp", "collect": "hosted", "resolve_url": "https://app.kaab.ist/r/…", … }
```

Then get the link to the person. With `"notify": "none"` you deliver it yourself, by e-mail, SMS or
in your own app; the `job.awaiting_input` webhook is the moment to do it, and
`input_request.resolve_url` on the job it points at is the link. The program language also accepts
`"notify": "email"` with a `"to"` address (a template such as `"{{inputs.email}}"`); this guide
delivers the link itself, which works whatever `notify` says.

Lint warns about a hosted input with no `hosted.notify` at all
([W007](https://docs.kaab.ist/reference/dsl/lint/w007)): it is a reminder that somebody has to tell the person the page
exists, or the job waits until its deadline.

The person opens the link, types the value, and the job continues. The value is checked against the
step's type and pattern before it is accepted, on the page and again by the API.

## Sharing a stopped job

A job that stopped for something unexpected (`needs_attention`) can be shared from its page in the
panel: **Share resolve link**. The dialog explains that anyone with the link can act on this one job
until it finishes or the park expires, and has one option, **Let them abort the job**, off by
default. The job's `attention.resolve_url` then holds the link.

The shared page shows the screenshot the job stopped on, a plain-sentence description of what is in
the way, the platform's suggested fixes (applied for this one job only), and **Open the page**: a
takeover of the remote browser with no address bar, no tabs and no session export.

## What the person sees

The page carries no Kaabist navigation and no sign-in. At the top, in small grey text, is your
organisation's name; under it the heading **We need one more thing from you** for an input, or
**We need your help to continue** for a stopped job. The footer reads "Secure page · the link works
for one task and expires", and the line under the card says when this link expires.

**On a phone** the page is a single column with a small margin on each side. For an input, the
screenshot of the page the job stopped on (when there is one) fills the width, followed by one
large field labelled with your `prompt` and a full-width **Send** button. The field is marked as a
one-time-code field, so phones offer a code that has arrived by SMS; a `number` input opens the
numeric keypad, and a `secret` input is masked. A value that does not match the pattern is refused
under the field before anything is sent. For a stopped job, each suggested fix is a short sentence
with an **Apply this fix** button below it, followed by **Or do it yourself** and an **Open the
page** button.

**On a desktop** the same content sits in a centred card at most 768 pixels wide, with more padding,
and each suggested fix has its **Apply this fix** button on the same line as its sentence. Opening
the page for a takeover widens the card to the full window so the remote browser has room.

When the shared link allows it, a red **Stop this task** button sits at the bottom of the card and
asks for confirmation ("It will not continue, and this link stops working").

After an answer the card is replaced by its outcome: **Thank you, it is on its way** with a spinner
while the job runs (the page checks again every few seconds), then **All done**. A link past its
deadline shows **This link has expired**, a withdrawn one or one whose job was canceled **This link
is no longer active**, and an address that is incomplete or unknown **This link does not work**.

## What a link can do

| | |
|---|---|
| One job | A link opens exactly one job, and acts only while that job waits. |
| Expiry | It expires with the job's park: an input's `expires`, or the attention's deadline (a week). |
| Revocation | It stops working when the job finishes or is canceled. |
| No account | Nothing about your organisation beyond its name is shown; no cookie is set. |
| Rate | 30 requests a minute per link. |
| Abort | Only on a link you shared with **Let them abort the job** ticked; never on an input's link. |
| Permanent changes | Never. A fix applied from the page affects this job only; making it permanent is an approval in your review queue. |

Pages are not indexed by search engines, send no referrer and are never cached. Every action taken
on one is recorded on the job.

## Worked example

Take a customer portal at `https://portal.example.com` that signs in with an e-mail address and a
password, then asks for a six-digit code. This program asks for the code on a hosted page:

### TypeScript

```ts
const definition = {
  kaab: "1",
  name,
  inputs: { email: "string", password: "secret" },
  options: { max_tier: "browser" },
  steps: [
    { goto: `${site}/login`, expect: { visible: "input[name=email]" } },
    { fill: "input[name=email]", value: "{{inputs.email}}" },
    { fill: "input[name=password]", value: "{{inputs.password}}" },
    { click: "form button[type=submit]", expect: { visible: "input[name=code]" } },
    {
      await_input: {
        name: "otp",
        prompt: "The six-digit code we sent you",
        pattern: "^[0-9]{6}$",
        collect: "hosted", // a page at input_request.resolve_url collects it
        hosted: { notify: "none" }, // or { notify: "email", to: "{{inputs.email}}" }
      },
    },
    { fill: "input[name=code]", value: "{{vars.otp}}" },
    { click: "form button[type=submit]", expect: { url: "**/dashboard" } },
    { extract: { heading: "h1" } },
  ],
};
await kaab.programs.create({ name, definition, publish: true });

const job = await kaab.jobs.create({
  program: name,
  inputs: { email: "dev@kaab.ist", password: "secret" },
});
// Without an onInput handler, wait returns as soon as the job parks.
const parked = await kaab.jobs.wait(job.id, { timeoutMs: 120_000 });
console.log(`${parked.id} is ${parked.status}: answer at ${parked.input_request?.resolve_url}`);
```

1. Create the job with `inputs: { "email": "dev@kaab.ist", "password": "<the account password>" }`.
2. It signs in and parks `awaiting_input`. `input_request.resolve_url` is set; the snippet prints it.
3. Open the link in a private window: the page names your organisation and asks for "The six-digit
   code we sent you". Type the code the site sent, for example `482913`, and press **Send**.
4. The page says **Thank you, it is on its way**, then **All done**; the job is `succeeded`, with
   the dashboard's heading in `data.heading`.

## Pitfalls

- **The link is a credential.** Whoever holds it can act on that job. Send it only to the person
  meant to answer, over a channel you trust, and never log it.
- **Answering twice.** Once a value is accepted, the page moves on; a second submission is refused
  because the job is no longer waiting.
- **Deadlines.** An input's `expires` is at most 7 days. Choose one that matches how quickly your
  users react; the browser itself is kept open only for the step's `ttl` (at most 15 minutes), and
  a later answer resumes from the last checkpoint.
- **Expecting the page to change the program.** Applying a fix from a hosted page never publishes
  anything. Review the proposal in the panel if it should apply to every job.

## Reference

- [`await_input`](https://docs.kaab.ist/reference/dsl/actions/await-input) and lint [W007](https://docs.kaab.ist/reference/dsl/lint/w007)
- [Get a job](https://docs.kaab.ist/reference/api/jobs/get-job) (`input_request`, `attention`), the
  [job schema](https://docs.kaab.ist/reference/api/schemas/job)
- [Events](https://docs.kaab.ist/reference/events): `job.awaiting_input`, `job.needs_attention`
- Concepts: [human in the loop](https://docs.kaab.ist/concepts/human-in-the-loop); panel: [takeover](https://docs.kaab.ist/panel/takeover)
