wrapflow-app
v0.3.74
Published
Typed, browser-safe client runtime for released Wrapflow Apps.
Maintainers
Readme
wrapflow-app
The zero-dependency, browser-safe runtime used by generated Wrapflow App clients. It talks only to a released App's typed contract and never accepts a Wrapflow account API token.
npm install --save-exact --ignore-scripts 'wrapflow-app@<reviewed-version>'
npm install --save-dev --save-exact --ignore-scripts 'wrapflow@<reviewed-version>'
node ./node_modules/wrapflow/wf.mjs app client <app-id> --file src/wrapflow.generated.tsReplace <reviewed-version> with one exact version whose registry metadata,
advisories, provenance, license, package contents, and publish age you reviewed.
Do not use a mutable tag such as latest; commit the resulting lockfile.
Invoke the reviewed project-local CLI by its package path as shown above. Do
not ask npx to resolve the wf binary: when the local dependency is absent,
it can resolve the unrelated registry package named wf.
wrapflow-app is the browser runtime dependency. The separate wrapflow
package provides the wf generation and operations CLI and is only needed on
development or build machines. An MCP client can invoke the equivalent App
client generation operation instead of installing the CLI directly.
Import the generated adapter in React, Angular, Vue, Tailwind, or plain TypeScript projects:
import { AppClient } from './wrapflow.generated'
async function main() {
const app = await AppClient.connect({
baseUrl: 'https://client-requests.wrapflow.app',
getToken: async () => currentAppUserJwt(),
})
await app.assign_request.execute(
{ owner: '[email protected]' },
{
recordId: 'request-123',
idempotencyKey: crypto.randomUUID(),
},
)
const report = await app.run_report.invoke({ region: 'eu', limit: 5 })
const active = await app.active_requests.query({ limit: 20 })
document.querySelector<HTMLDivElement>('#app')!.textContent =
JSON.stringify({ report, active }, null, 2)
}
void main()In plain Vite projects, keep startup awaits inside an async entry function so the default production target builds without enabling top-level await.
The generated adapter pins the App Contract compatibility hash and imports
this package for transport, authentication, version checks, and stable typed
errors. Named filtered views are generated as typed query resources; their
declared predicates are enforced by the App before any narrower caller filters.
Workflow invocation uses the Contract-v2 { params } envelope and
semantic media type internally; do not hand-write that transport. Do not put a
Wrapflow account token in browser code.
Checkout uses that same typed transport. For anonymous idempotent checkout,
persist the returned ownershipToken privately with the idempotency key and
pass both after a reload; authenticated Apps obtain their App-user token from
the configured token/getToken option. Capabilities are headers, never URL
parameters or log fields.
Run an account-owner command on the uploaded draft
An uploaded page uses its generated client on the current App origin. Open the
fresh signed draft link returned by the upload. This example's App has no hosted
App-user sign-in configured and uses the owner's draft authority.
The link itself carries its issuer's write authority: anyone holding it can
exercise those writes without a Wrapflow account session. Protect the link as a
credential; an account session alone does not authorize the draft host.
The host supplies the preview authority after checking the link.
Your page does not need an account API token or a second owner login.
For an articles entity and a create-article create command with
access: { audience: 'owner-only', responseFields: ['slug'] } and the four
input fields below, generate the client from the owner's inspected Contract:
import { AppClient } from './wrapflow.generated'
async function createArticle(slug: string, title: string, body: string) {
const app = await AppClient.connect({ baseUrl: window.location.origin })
return app.create_article.execute(
{ slug, title, body, published: false },
{ idempotencyKey: crypto.randomUUID() },
)
}Call createArticle(slug, title, body) from your form handler and show the
result or refused error. It saves a private article; a separate declared
command would publish it. This example assumes the generated command getter
is create_article; use the name your generated file actually declares.
A caller without this owner draft authority cannot run the command. An App-user
principal, even with a role named owner, cannot run an account-owner command.
Hosted App-user sign-in on an uploaded page
After the account owner approves hosted sign-in, inspect
contract.authentication. For mode: 'hosted-email-code', bundle this helper
with the generated client:
import { AppClient } from './wrapflow.generated'
async function startSignIn(email: string) {
const app = await AppClient.connect({ baseUrl: window.location.origin })
await app.auth.requestCode(email)
return async function finishSignIn(code: string) {
await app.auth.verifyCode(email, code)
return app
}
}On the Send code action, call startSignIn(emailFromForm) and retain the
returned finishSignIn. When the person enters the emailed code, call
finishSignIn(codeFromForm) and reuse its returned app for queries and
commands. The client retains the App-user token and attaches it to subsequent
calls; do not construct auth routes or bearer headers. Keep the code-entry UI
and error state in your page. The emailed code needs a real mailbox; draft test
users rehearse roles without accessing anyone's email.
Live account-owner boundary: this signs an App user in. It supports
commands granted to that user's App roles through allowedRoles; it does not
establish Wrapflow account ownership for access.audience: 'owner-only'.
Account-owner commands require the owner's Wrapflow account authority and are
omitted from the live public Contract. There is no generated-client browser
account-owner sign-in method. Use the owner draft path above for that example;
do not copy an account API token into the page or silently weaken its command
policy. An App with no authentication has no app.auth sign-in capability.
Read one article with its long text
For an articles entity with a unique slug key and a body text field declared
with maxLength: 500000, use a declared Flow filter view named Published articles
with the expression published == true directly after the articles table.
In exported flow text, the filter clause is
let published_articles = filter(input: articles, where: published == true);
keep the saved label/metadata and inspect the resulting Contract resource name.
Generate its client; this example assumes its getter is published_articles.
This view accepts narrower queries while its published-only filter stays on
the server. The Contract must make slug, title and body readable.
Put <article id="article"></article> in your page. In its bundled TypeScript,
load one article by its short key and select the whole body explicitly:
import { AppClient } from './wrapflow.generated'
async function loadArticle(slug: string) {
const app = await AppClient.connect({ baseUrl: window.location.origin })
const { items } = await app.published_articles.query({
where: [{ field: 'slug', op: 'eq', value: slug }],
select: ['slug', 'title', 'body'],
limit: 1,
})
const article = items[0]
if (!article) throw new Error('Article not found.')
document.querySelector<HTMLElement>('#article')!.textContent = article.body
}Call loadArticle('first') from your page's async startup or click handler and
show any rejected error to the reader. On an uploaded page, the current origin
addresses its draft or released backend. For a separately hosted frontend,
replace it with the Wrapflow App's backend URL.
Lists omit long text unless select names it; the response reports
omittedFields. limit: 1 alone does not include it. Filter on the unique
short key, never on the long body (long text cannot be filtered, sorted,
searched or grouped). No matching visible row means missing or unpublished;
handle that state without reading the private table. A fixed view created with
define kind:"view" instead accepts only ordering, paging and counting: do not
send it where or select, or assume it accepts a slug parameter. Inspect the
Contract and verify the draft before choosing the read resource. If your inspected
Contract exposes an entity query, query({ recordId, select: ['body'] })
also reads one record; use the declared resource rather than inventing a route.
Hosted single-file Wrapflow Apps already receive the matching same-origin runtime and do not need this package.
