# @t4h.framework/dev

Local development runtime for [T4H Framework](https://github.com/tech4humans-brasil/framework) apps. Watches your project, compiles TypeScript, loads your `App` in a child process and exposes it over HTTP with the same routes as the production runtime — no external infrastructure required.

## What it does

When you run `framework dev` (via `@t4h.framework/cli`), this package powers the experience behind the scenes:

1. **TypeScript watch** — compiles your app on every change.
2. **App process** — forks a child Node process that loads your compiled `App` and runs the workflow engine on an in-memory SQLite database.
3. **HTTP server** — a [Hono](https://hono.dev) app that exposes your workflows (create runs, advance pending activities), enforces the workflow authorization and adds `/_dev` routes for tooling.
4. **Vite** — a Vite dev server in middleware mode serves the built-in panel and anything else that is not an API route.
5. **Dev claims** — provides local implementations for ORM, HTTP, file system, cache, SMTP, and more.

```mermaid
flowchart TB
  subgraph parent ["Parent process (Server)"]
    Watch["TypeScript watch"]
    HTTP["Hono (/projects, /_dev)"]
    Vite["Vite middlewares (UI)"]
    Auth["Workflow authorization"]
    Watch -->|"build success"| Fork["AppProcess.up()"]
    HTTP --> Auth
  end

  subgraph child ["Child process (app-process worker)"]
    App["User App"]
    Engine["Workflow engine + SQLite :memory:"]
    Claims["Dev claims"]
    App --> Engine
    Claims --> App
  end

  Fork --> App
  HTTP <-->|"IPC / JSON-RPC"| Engine
```

## Installation

This package is typically consumed as a dependency of `@t4h.framework/cli`:

```bash
yarn add -D @t4h.framework/cli
```

To use it programmatically:

```bash
yarn add -D @t4h.framework/dev
```

### Peer dependencies

Claims are matched by constructor identity, so `@t4h.framework/dev` resolves the
framework packages from your project instead of installing its own copies. A
second copy makes the dev server fail with
`No provider for "..." found for claim key "..."`.

Required:

- `@t4h.framework/core`

Optional — loaded only when your project has them, and skipped otherwise, so a
workflow that never imports a package costs nothing:

- `@t4h.framework/cache`
- `@t4h.framework/fs`
- `@t4h.framework/http`
- `@t4h.framework/orm`
- `@t4h.framework/runtime-claims`
- `@t4h.framework/sftp`
- `@t4h.framework/smtp`
- `@t4h.framework/webhook` — the callbacks route is part of the core and works
  for any async activity; the package only adds the `webhook()` activity

Two public routes advance a waiting async activity from outside, both without
project authorization: `POST /projects/:projectId/workflows/:workflowId/runs/:runId/activities/:activityId/callbacks?token=…`
(the URL an activity hands to a third party) and
`POST /projects/:projectId/workflows/:workflowId/events` (a fixed URL for third
parties that emit every event to one place; the activity's `toCorrelationKey`
matches the event to a `pending({ correlationKey })`).

- `@t4h.framework/hmac`, `@t4h.framework/jwks`, `@t4h.framework/oidc` — only
  when a workflow declares the matching authorization, or an async activity
  declares an `hmac` or `jwt` callback `auth`

## CLI usage

Add a dev script to your project's `package.json`:

```json
{
  "scripts": {
    "dev": "framework dev"
  }
}
```

Then run:

```bash
yarn dev
```

The server listens right away and prints the routes of every workflow once the first build finishes.

### Options

| Flag                          | Description                                                                                                                          | Default          |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------- |
| `--entrypoint <path>`         | Path to the project's `package.json`                                                                                                 | `./package.json` |
| `-e, --env <KEY=VALUE>`       | Environment variable (repeatable)                                                                                                    | —                |
| `--env-file-if-exists <path>` | Load variables from a `.env` file if it exists                                                                                       | —                |
| `-p, --port <port>`           | HTTP port (also `PORT`); when something already answers on it, on any address of `--host`, the next free port up is taken and logged | `3000`           |
| `--host <host>`               | HTTP host                                                                                                                            | `localhost`      |

Example with env vars:

```bash
framework dev --entrypoint ./package.json -e API_KEY=secret -e DB_HOST=localhost
```

## HTTP routes

The dev server has no tenants and a single live version (the last build), so the routes carry only the project and the workflow. `projectId` must match your `App` id.

| Method  | Route                                                                           | Authorization | Response                   |
| ------- | ------------------------------------------------------------------------------- | ------------- | -------------------------- |
| `GET`   | `/projects/:projectId/manifest.json`                                            | no            | `200` the app manifest     |
| `POST`  | `/projects/:projectId/workflows/:workflowId/runs`                               | yes           | `201 { "id": "<run id>" }` |
| `PATCH` | `/projects/:projectId/workflows/:workflowId/runs/:runId/activities/:activityId` | yes           | `200 { id, type, status }` |

The `POST` body is the workflow input, validated against the workflow schema (`400` with the validation errors). The `PATCH` body is `{ "payload": ... }`, validated against the pending async activity schema (`400 Payload is invalid`, `400 AsyncActivity is not pending`). Errors use the NestJS shape `{ statusCode, message, error }`; `503` means the app process is still starting.

```bash
curl -X POST http://localhost:3000/projects/my-app/workflows/my-workflow/runs \
  -H 'content-type: application/json' \
  -H 'x-signature: <hmac>' \
  -d '{ "userId": "123" }'
```

### Authorization

Every `/projects` route requires an authorization: the workflow `authorization` wins over the app one, its manifest `pathToModule` is imported and its default export receives the manifest `context` templated with the request (`<< headers.x-signature >>`, `<< rawBody >>`, `<< body.id >>`, `<< params.projectId >>`, `<< query.token >>`). A falsy result or a thrown error answers `401`. A workflow without any authorization is refused when the manifest arrives (the server emits `error`) and answers `500` if reached.

`pathToModule` may be `@t4h.framework/hmac/authorize`, `@t4h.framework/jwks/authorize`, `@t4h.framework/oidc/authorize`, an absolute path, a path relative to the project or a `file://` URL.

### Dev routes (no authorization)

| Method | Route                                                                                                               | Description                                                                                                                     |
| ------ | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `GET`  | `/_dev/manifest`                                                                                                    | App manifest                                                                                                                    |
| `GET`  | `/_dev/state`                                                                                                       | `{ state: 'idle' \| 'starting' \| 'ready' \| 'exited' }`                                                                        |
| `GET`  | `/_dev/runs?status=&workflow=&id=&parentRun=&parentActivity=&rootOnly=&createdAfter=&createdBefore=&limit=&offset=` | `{ count, runs }` newest first; `id` is a prefix, `rootOnly=true` hides child runs, `createdAfter`/`createdBefore` are ISO 8601 |
| `GET`  | `/_dev/runs/:id`                                                                                                    | Run with `input` and `output`; a rejected run carries its reason as `output` (framework tagged JSON)                            |
| `GET`  | `/_dev/activities/waiting`                                                                                          | Async activities waiting for an advance                                                                                         |
| `POST` | `/_dev/workflows/:workflowId/runs`                                                                                  | Starts a run without authorization; body is the input, answers `201 { id }`                                                     |
| `POST` | `/_dev/activities/:activityId/advance`                                                                              | Advances a pending async activity without authorization; body is `{ payload }`                                                  |
| `GET`  | `/_dev/events`                                                                                                      | SSE: `manifest`, `build:status`, `build:fail`, `app:exit`, `run:created`, `run:updated`                                         |

`run:created` and `run:updated` carry the run summary (the same object `/_dev/runs` lists); updates of one run are coalesced, so a tick that writes several rows produces one event.

Everything else is handed to Vite, which serves the panel that ships with this package: open the base URL to browse the workflows and start runs from their schema, follow runs live with the execution trace of each one (activities as a waterfall, child runs nested, input, output, duration, logs, the activity that failed) and advance waiting activities. Plugins from the project's `vite.config.*` (or `vite.plugins`) are served by the same Vite.

## State

Workflow runs, activities, pending async activities and logs live in an in-memory SQLite database (`node:sqlite`) inside the app process. Every successful build forks a fresh process, so **a rebuild resets all runs, timers and pending activities**; the `manifest` SSE event tells a UI to reload. There is no durability, queue or resume in the dev server by design.

Sync activities run inline, `sleep` and other timers use `setTimeout`, and child workflows started with `startWorkflow` / `startWorkflowMany` run in parallel.

The ORM claim is the exception: its rows live in `db.sqlite` next to your `package.json` and survive rebuilds and restarts (add it to `.gitignore`). Delete the file to start over. The previous `db.json` is no longer read.

## Programmatic usage

```typescript
import { Server } from '@t4h.framework/dev'

const server = new Server({
  entrypoint: './package.json',
  env: { API_KEY: 'secret' },
  port: 3000,
  host: 'localhost',
  vite: { plugins: [myPanelPlugin()] },
})

server.on('manifest', manifest => {
  console.log(
    'App ready:',
    manifest.workflows.map(workflow => workflow.id),
  )
})

const address = await server.up()

console.log(address.url)

await server.down()
```

### Server API

| Member     | Description                                                                                                                                              |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `up()`     | Starts HTTP + Vite, then the TypeScript watch; resolves with `{ host, port, url }`. `port` is the one actually bound: a port already in use moves it up. |
| `down()`   | Stops the watch, the app process, Vite and the HTTP server.                                                                                              |
| `manifest` | Current app manifest or `null`.                                                                                                                          |
| `address`  | Listening address (throws before `up()`).                                                                                                                |
| events     | `manifest`, `build:status`, `build:fail`, `app:exit`, `run:created`, `run:updated`, `error`.                                                             |

`vite: false` disables Vite; `vite.root` / `vite.configFile` point at another Vite project. The server logs the listening URL, build output and the routes of every workflow through `logger` (default `console`, `false` to silence). A manifest with a workflow that has no authorization (and no app default) emits `error` with a `WorkflowAuthorizationMissingError`; `framework dev` exits with code 1 in that case.

## Dev claims

In production, claims are fulfilled by the runtime (database connections, real HTTP clients, etc.). During local development, `@t4h.framework/dev` registers in-memory substitutes:

| Claim                    | Dev implementation                                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `ORMClaim`               | SQLite file `db.sqlite` in the project directory; `where` keeps the Mongo-like semantics (`$like`, `$ilike`, `$or`, ...) |
| `HttpClientRequestClaim` | Real HTTP via `undici`                                                                                                   |
| `FileSystemClaim`        | Local file system with dev binary codec                                                                                  |
| `SmtpSendEmailClaim`     | Captures emails in memory (no delivery)                                                                                  |
| `CacheClaim`             | In-memory cache                                                                                                          |
| `WorkflowClaim`          | Runs workflows through the local workflow engine                                                                         |

Import the full claim registry:

```typescript
import { claims } from '@t4h.framework/dev'
```

Or import individual claim classes:

```typescript
import {
  DevORMClaim,
  DevHttpClientRequestClaim,
  DevFileSystemClaim,
  findClaimByConstructor,
} from '@t4h.framework/dev'
```

## Requirements

- Node.js >= 22.13 (`node:sqlite`)
- A T4H Framework `App` exported from your project's entry point (`default` or named `app` export)

## License

MIT
