From 8b9d467b0d7f779c06922d7842114e14fe7cc0d7 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 2 Sep 2026 08:28:14 -0600 Subject: [PATCH] docs: fix four v5 reference errors surfaced by skills#81 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each of these is a real error in v5 reference content that a `mode: generate` skill rule reproduced faithfully, so agents were being taught the broken pattern. - `database/api.md` — the `transaction()` timer example did not await the transaction, so a failed write became an unhandled rejection the callback never observed. Awaited, wrapped in try/catch/finally, and given an overlap guard for jobs that can outlast their interval. Also added the missing `transaction` and `isMainThread` imports. - `security/jwt-authentication.md` — the "no `Authorization` header is required" claim is true only for the username/password flow; it is now scoped to that shape. The scoped-token example showed no authentication even though the mint requires an authenticated super_user, so it now shows Basic Auth and names the `server.operation(..., true)` alternative. - `rest/overview.md` — the `rest: true` requirement contradicted its own no-configuration-file carve-out. `@export` is always required; `rest: true` is required whenever a `config.yaml` exists. - `components/applications.md` — the fetchable-ref constraint read as an absolute rule and so implied that valid SHA-based rollbacks are rejected. The full-commit-SHA exception is now stated in that paragraph. Refs HarperFast/skills#81 Co-Authored-By: Claude Opus 5 --- reference/components/applications.md | 2 +- reference/database/api.md | 29 ++++++++++++++++++------ reference/rest/overview.md | 6 ++--- reference/security/jwt-authentication.md | 20 ++++++++++++++-- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/reference/components/applications.md b/reference/components/applications.md index 19b0354b..7b725ef1 100644 --- a/reference/components/applications.md +++ b/reference/components/applications.md @@ -172,7 +172,7 @@ harper deploy ref=9f8c2a1 restart=true replicated=true If a `ref` can't be resolved either way, the deploy stops rather than sending the name for the cluster to resolve. Run `git fetch` and retry, or pass a full commit SHA — that needs no resolution and is always accepted. -A `ref` must also name something a clone can fetch: `refs/heads/*` and `refs/tags/*`, or a bare branch or tag name. Anything else — `refs/pull/123/head`, say — is rejected up front, even if your own checkout can resolve it, because the cluster could resolve that commit and still never check it out. +**A full commit SHA is accepted directly.** An object ID is already immutable, so there is nothing to pin it to and no resolution is attempted — SHA-based rollbacks are always valid. Every other `ref` must name something a clone can fetch: `refs/heads/*` or `refs/tags/*` if you qualify it, or a bare branch or tag name that resolves locally or on the remote. A qualified ref outside those two namespaces — `refs/pull/123/head`, say — is rejected up front, even if your own checkout can resolve it, because the cluster could resolve that commit and still never check it out. **Commit and push first.** The cluster clones from the remote, so it only sees commits that have been pushed. `by_ref` warns in both directions: when the working tree is dirty (those changes won't be part of the deploy) and when the commit being deployed isn't on any remote branch (the cluster won't be able to clone it). The second check reads your local remote-tracking refs, so run `git fetch` if you get it for a commit you know you pushed. diff --git a/reference/database/api.md b/reference/database/api.md index 16d373e3..17d71b77 100644 --- a/reference/database/api.md +++ b/reference/database/api.md @@ -104,21 +104,36 @@ For most operations — HTTP request handlers, for example — Harper automatica ### Basic Usage ```javascript -import { tables } from 'harper'; +import { isMainThread } from 'node:worker_threads'; +import { tables, transaction } from 'harper'; const { MyTable } = tables; if (isMainThread) { + let running = false; setInterval(async () => { - let data = await (await fetch('https://example.com/data')).json(); - transaction(async (txn) => { - for (let item of data) { - await MyTable.put(item, txn); - } - }); + if (running) return; // the previous run has not committed yet + running = true; + try { + let data = await (await fetch('https://example.com/data')).json(); + await transaction(async (txn) => { + for (let item of data) { + await MyTable.put(item, txn); + } + }); + } catch (error) { + logger.error('hourly import failed', error); + } finally { + running = false; + } }, 3600000); // every hour } ``` +Two details matter outside a request context: + +- **`await` the `transaction()` call, and catch it.** An unawaited call lets the timer callback finish before the commit resolves, so a failed write is never observed; awaiting without a `catch` still leaves the rejection unhandled, because nothing awaits the callback itself. +- **Guard against overlap when a job can outlast its interval.** A timer fires on schedule regardless of whether the previous run has committed, so two runs would open two transactions over the same rows. The `running` flag above skips a tick instead. + ### Nesting If `transaction()` is called with a context that already has an active transaction, it reuses that transaction, executes the callback immediately, and returns. This makes `transaction()` safe to call defensively to ensure a transaction is always active. diff --git a/reference/rest/overview.md b/reference/rest/overview.md index ddc602ed..252d0226 100644 --- a/reference/rest/overview.md +++ b/reference/rest/overview.md @@ -39,8 +39,8 @@ rest: This section describes the **default table Resource** — the endpoints Harper registers automatically for a table, with no handler code of your own. Harper serves that default Resource only when **both** of the following are true: -1. The table is exported in a schema definition with [`@export`](../database/schema.md#export). -2. REST is enabled for the application — normally `rest: true` in `config.yaml` (see [Configuration](#configuration)); a component directory with **no configuration file at all** gets it from Harper's built-in default instead, as described below. +1. The table is exported in a schema definition with [`@export`](../database/schema.md#export). This is always required. +2. REST is enabled for the application. `rest: true` must be present in `config.yaml` **whenever a configuration file exists** (see [Configuration](#configuration)); a component directory with no configuration file at all inherits `rest` from Harper's built-in default instead, as described below. ```graphql # schema.graphql @@ -58,7 +58,7 @@ graphqlSchema: rest: true ``` -Neither half is sufficient on its own. Without `@export` Harper registers no default Resource for the table, so it has no REST route and callers get `404`. Without REST enabled the REST handler is never registered for the application, so even an exported table does not respond to HTTP requests. +Neither half is sufficient on its own. Without `@export` Harper registers no default Resource for the table, so it has no REST route and callers get `404`. Without REST enabled the REST handler is never registered for the application, so even an exported table does not respond to HTTP requests. Writing `rest: true` is what enables it in a `config.yaml` — the only way REST is enabled without that line is the no-configuration-file case below. `@export` is how the **table itself** claims the URL. When a JavaScript subclass of `tables.MyTable` should own that URL instead, omit `@export` from the schema and export the class — the class claims the route and serves whatever it implements, and REST still has to be enabled. Leaving `@export` on the schema while also exporting a same-named subclass produces conflicting endpoints. See [Extending a Table](../resources/overview.md#extending-a-table). diff --git a/reference/security/jwt-authentication.md b/reference/security/jwt-authentication.md index 4ae7ed9a..8400bef7 100644 --- a/reference/security/jwt-authentication.md +++ b/reference/security/jwt-authentication.md @@ -18,7 +18,7 @@ JWT authentication uses two token types: ## Create Authentication Tokens -Call `create_authentication_tokens` with your Harper credentials. No `Authorization` header is required for this operation. +Call `create_authentication_tokens` with your Harper credentials. When the request carries a `username` and `password` in the body, no `Authorization` header is required — the operation authenticates from the body. Other shapes of this operation do need an authenticated caller: sending no credentials at all mints tokens for the user the request is already authenticated as, and [minting a scoped token](#scoped-tokens-inline-role) requires an authenticated `super_user`. ```json { @@ -95,7 +95,7 @@ Available since: v5.2.0 A super user can mint a **scoped token**: a single JWT whose permissions are embedded in the token itself, so the bearer needs no pre-existing user or role record. This is useful for handing a limited credential (for example, read-only access) to an external service or script without provisioning it in `hdb_user`. -Pass `role` as an inline role-shaped object (the same `permission` structure used by [`add_role`](../users-and-roles/overview.md), including the `operations` allowlist). The request must be authenticated as a `super_user`; no `password` may be included: +Pass `role` as an inline role-shaped object (the same `permission` structure used by [`add_role`](../users-and-roles/overview.md), including the `operations` allowlist). Unlike the username/password flow above, this shape carries no credentials of its own — the **minter** must be authenticated as a `super_user`, and no `password` may be included in the body: ```json { @@ -115,6 +115,22 @@ Pass `role` as an inline role-shaped object (the same `permission` structure use } ``` +Authenticate that request the way you would any other privileged operation — Basic Auth with a `super_user`'s credentials, or a `Bearer` `operation_token` already held by one: + +```bash +curl --location --request POST 'http://localhost:9925' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Basic ' \ + --data-raw '{ + "operation": "create_authentication_tokens", + "username": "reporting-service", + "role": { "permission": { "operations": ["read_only"] } }, + "expires_in": "7d" + }' +``` + +Without an authenticated `super_user` the mint is rejected with `403 Only super_user can create a token with an inline role`. From inside a component, pass the request context and `authorize: true` to [`server.operation()`](../http/api.md#serveroperationoperation-context-authorize) so the mint is attributed to — and permission-checked against — the calling user. + Response: ```json