@bowmark/web 0.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +273 -0
- package/package.json +44 -1
- package/src/generated/library.d.ts +10490 -0
- package/src/generated/validators.ts +10440 -0
- package/src/guard.ts +198 -0
- package/src/index.ts +71 -0
- package/src/session.ts +194 -0
- package/src/transport.ts +342 -0
- package/src/validate.ts +371 -0
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# @bowmark/web
|
|
2
|
+
|
|
3
|
+
> **Status:** PUBLISHED · `@bowmark/web@1.0.0` on npm, 2026-08-06, alongside
|
|
4
|
+
> `bowmark-web` and `bowmark-web-stubs` on PyPI at the same version. The plan that built it
|
|
5
|
+
> is deleted; the reasoning is in the four
|
|
6
|
+
> [`docs/decisions/2026-08-06-*`](../../../docs/decisions/) records and the enforceable half
|
|
7
|
+
> is [`.claude/rules/public-types.md`](../../../.claude/rules/public-types.md).
|
|
8
|
+
> **What would make this doc wrong:** a build step appearing, the declarations moving out
|
|
9
|
+
> into a second package, or a runtime dependency landing in `package.json`.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm i @bowmark/web
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**There is a Python client too**, from this same directory tree and the same generated
|
|
16
|
+
surface: `pip install bowmark-web bowmark-web-stubs`
|
|
17
|
+
([`../python/README.md`](../python/README.md)). One `LibraryManifest` produces the
|
|
18
|
+
declarations below AND the `.pyi`, `gate:public-types` asserts the two languages type the
|
|
19
|
+
same functions, and all three packages ship at one version — so a caller in either
|
|
20
|
+
language is looking at the same library.
|
|
21
|
+
|
|
22
|
+
Real TypeScript for the whole `bowmark.*` surface, with no Bowmark source on the
|
|
23
|
+
caller's disk. Their code runs in their editor and their process; the capability calls
|
|
24
|
+
execute on our servers.
|
|
25
|
+
|
|
26
|
+
**One package, not two.** The generated declarations ship inside this one rather than
|
|
27
|
+
beside it in a `@bowmark/catalog-types`, because installing one thing and getting
|
|
28
|
+
autocomplete is the entire promise. What exists to call is listed, one row per
|
|
29
|
+
function, in
|
|
30
|
+
[`CAPABILITIES.md`](https://github.com/bowmark-ai/web/blob/main/CAPABILITIES.md) and
|
|
31
|
+
[`PROVIDERS.md`](https://github.com/bowmark-ai/web/blob/main/PROVIDERS.md) — absolute
|
|
32
|
+
URLs, because this README is read on npm where a relative link resolves to nothing.
|
|
33
|
+
|
|
34
|
+
**Zero runtime dependencies, permanently.** Not an aspiration — the argument guard is
|
|
35
|
+
plain JS rather than a validator library, and the envelope shapes are hand-restated
|
|
36
|
+
rather than imported from `@bowmark/schema`, for exactly this reason. A `@bowmark/*`
|
|
37
|
+
import here would compile and typecheck in this repo and fail to install for every
|
|
38
|
+
consumer outside it; `tests/unit/bowmark-web-envelopes.test.ts` refuses one.
|
|
39
|
+
|
|
40
|
+
## The session is the surface
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { session } from "@bowmark/web";
|
|
44
|
+
|
|
45
|
+
const itemCount = await session(async (bm) => {
|
|
46
|
+
const found = await bm.providers.gymshark.search({ query: "hoodie" });
|
|
47
|
+
await bm.providers.gymshark.addToCart({ variantId: found.products[0].variantId });
|
|
48
|
+
return (await bm.providers.gymshark.getCart()).itemCount; // 1
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Your control flow stays on your machine. Real `if`, real `for`, real closures, real
|
|
53
|
+
autocomplete, and **no script string**. Each capability call is one round trip into one
|
|
54
|
+
live instance on ours — that is stated rather than hidden, because a surface that looks
|
|
55
|
+
like a local function call and is actually stateful is how a caller writes an N+1
|
|
56
|
+
without noticing. The session closes in a `finally`, so a throw inside the callback
|
|
57
|
+
still releases it.
|
|
58
|
+
|
|
59
|
+
**The callback is never stringified and never shipped.** `toString()` on your function
|
|
60
|
+
returns whatever your build tooling emitted — istanbul's `cov_…f[2]++`, esbuild's
|
|
61
|
+
`__name`, TypeScript downleveling's `tslib_1` — so a failure would live in a toolchain
|
|
62
|
+
we cannot see. Playwright was asked to fix that class of bug and formally declined.
|
|
63
|
+
|
|
64
|
+
### The three other entry points
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { bowmark, client, openManagedSession, run } from "@bowmark/web";
|
|
68
|
+
|
|
69
|
+
await bowmark.music.search("aphex twin", 25); // one-shot: its OWN session, opened and closed
|
|
70
|
+
const bm = client({ apiKey: "bmk_…" }); // the same, configured
|
|
71
|
+
const held = await openManagedSession(); // a session with the `finally` moved to you
|
|
72
|
+
const envelope = await run("return await bowmark.music.search('x')"); // the agent path
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**`bowmark` and `client()` are correct for ONE call and wrong for several.** Two calls
|
|
76
|
+
get two instances and two cookie jars, so a cart filled by the first does not exist for
|
|
77
|
+
the second — and the failure is silent: Shopify answers `POST /cart/add.js` with 200 and
|
|
78
|
+
the added line echoed back, then reports `item_count: 0`. Reach for `session()` the
|
|
79
|
+
moment a flow has a second step.
|
|
80
|
+
|
|
81
|
+
**`run(string)` is untyped by construction** and stays as the agent path. A template
|
|
82
|
+
literal gets no typechecking, so the generated types cover `session()` and `bowmark`
|
|
83
|
+
and never this. It returns the envelope rather than throwing, because a script is
|
|
84
|
+
composite: `status`, `logs` and `result` are read together.
|
|
85
|
+
|
|
86
|
+
### What it throws
|
|
87
|
+
|
|
88
|
+
| Class | When | What to do |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `BowmarkNeedsUserError` | One call paused for a human login | Open `err.handoff.url`, then **call again — the session is still open** |
|
|
91
|
+
| `BowmarkError`, `code: "wire_refused"` | An argument the wire cannot carry | Fix the argument; the message names the exact path, `args[0].when.checkIn` |
|
|
92
|
+
| `BowmarkError`, `code: "bad_argument"` | An argument the declared signature does not accept | Same — the message names the path and what was expected |
|
|
93
|
+
| `BowmarkError`, `code: "unknown_function"` | A KNOWN unit, and no such function on it in this package's declarations | Check the name, or upgrade — the library may have grown it since this version |
|
|
94
|
+
| `BowmarkError`, any other `code` | The call failed, or the transport did | Branch on `code`; `error` is prose written for an agent, `code` is for your `catch` |
|
|
95
|
+
|
|
96
|
+
`needs_user` is a **status, not an error**, and it is a separate class for that reason:
|
|
97
|
+
an agent that reads a failure retries, and retrying a login halt buys the same halt.
|
|
98
|
+
|
|
99
|
+
### Configuration
|
|
100
|
+
|
|
101
|
+
`BOWMARK_API_KEY` and `BOWMARK_API_URL`, read at CALL time, or passed explicitly as
|
|
102
|
+
`{ apiKey, baseUrl, fetch, headers, signal, onLog }`. A caller header cannot displace
|
|
103
|
+
the key. Anonymous is legal — every browserless capability works without one, on a
|
|
104
|
+
smaller daily budget.
|
|
105
|
+
|
|
106
|
+
## The two halves, and why they are split
|
|
107
|
+
|
|
108
|
+
- **`src/{index,session,transport,guard,validate}.ts`** — hand-written, ~700 lines,
|
|
109
|
+
changes almost never.
|
|
110
|
+
- **`src/generated/library.d.ts`** — every capability and provider, generated,
|
|
111
|
+
committed, regenerated whenever a unit lands.
|
|
112
|
+
- **`src/generated/validators.ts`** — the same functions' argument shapes as DATA, for
|
|
113
|
+
the runtime half of the same job.
|
|
114
|
+
|
|
115
|
+
The generated half is **ambient**: it declares globals and exports nothing, and
|
|
116
|
+
`index.ts` reaches it with a `/// <reference>`. An `import` would make it a module, every
|
|
117
|
+
declaration in it would stop being global, and the churn split would buy nothing — the
|
|
118
|
+
types would become a real dependency of the client. `@cloudflare/workers-types` and
|
|
119
|
+
`@types/node` are the precedent.
|
|
120
|
+
|
|
121
|
+
`index.ts` therefore **aliases** rather than re-exports (`export type Library =
|
|
122
|
+
BowmarkLibrary`). A global from an ambient file is not a local declaration, so
|
|
123
|
+
`export type { BowmarkLibrary }` is `TS2661`.
|
|
124
|
+
|
|
125
|
+
**The same declarations describe TWO implementations, deliberately.** `bowmark` exported
|
|
126
|
+
here is a real Proxy over HTTP in the caller's process; `bowmark` inside a `run()` script
|
|
127
|
+
is the sandbox's own global (`packages/runtime/src/namespace.ts`) in an isolate on ours.
|
|
128
|
+
The types are generated once and describe both, so the two must stay in sync.
|
|
129
|
+
|
|
130
|
+
**The Proxy accepts every name at runtime** — `bowmark.anything.at.all()` builds a path
|
|
131
|
+
and sends it. That is not a bug; it is why the generated types are load-bearing rather
|
|
132
|
+
than decorative. The Proxy makes a correct call work without enumerating half a million
|
|
133
|
+
names, and TypeScript makes a wrong one a compile error. Neither half is sufficient
|
|
134
|
+
alone. It does refuse two things locally: a path shorter than `<unit>.<fn>`, and an
|
|
135
|
+
argument the wire cannot carry.
|
|
136
|
+
|
|
137
|
+
**A proxy node answers `undefined` for `then`, `catch`, `finally` and `toJSON`.** Without
|
|
138
|
+
that, `await bowmark.music` would find a callable `then`, invoke it as a thenable, and
|
|
139
|
+
hang forever waiting for a resolve a path segment can never call.
|
|
140
|
+
|
|
141
|
+
## The wire guard is a checked COPY
|
|
142
|
+
|
|
143
|
+
`src/guard.ts` duplicates `wireProblem` from `packages/schema/src/wire.ts`, because
|
|
144
|
+
importing the workspace package would break the tarball for everyone outside this repo.
|
|
145
|
+
A copy drifts, so `tests/unit/bowmark-web-guard.test.ts` runs both over one fixture
|
|
146
|
+
table and asserts identical `{ path, reason }` for every entry. Add a refusal to
|
|
147
|
+
`wire.ts` and this fails by name.
|
|
148
|
+
|
|
149
|
+
It is a walk, not a `try { JSON.stringify(args) }`: stringify throws on exactly two
|
|
150
|
+
things (a circular structure and a `BigInt`) and silently mangles everything else —
|
|
151
|
+
`Date` → string, `Map`/`Set` → `{}`, class instance → plain object, function-valued key
|
|
152
|
+
→ dropped. Temporal shipped that exact bug, diagnosed it as a typing problem, and closed
|
|
153
|
+
it won't-fix.
|
|
154
|
+
|
|
155
|
+
## The argument guard is TWO guards, in this order
|
|
156
|
+
|
|
157
|
+
`assertWireSafeArgs` asks whether the value can cross at all — a `Date`, a `Map`, a
|
|
158
|
+
function, a circular structure. `assertArgShape` asks whether it matches what the
|
|
159
|
+
function declares. Wire first, always: shape-first would report a `Date` as "expected a
|
|
160
|
+
string" and send the caller after the wrong bug.
|
|
161
|
+
|
|
162
|
+
The shape half is `src/generated/validators.ts` (data, generated from the same manifest
|
|
163
|
+
as the declarations) read by `src/validate.ts` (one interpreter, ~250 lines). Not 192
|
|
164
|
+
emitted functions: 192 emitted functions are 192 pieces of code no test ever runs, and
|
|
165
|
+
one interpreter over a fixture table is testable. No validator library on either side —
|
|
166
|
+
zero runtime dependencies is the product.
|
|
167
|
+
|
|
168
|
+
**Every rule leans toward ACCEPTING, and that is the design.** A false refusal is a
|
|
169
|
+
caller whose correct argument their own client rejects, with no server to appeal to. A
|
|
170
|
+
false accept costs one round trip and lands them exactly where they were before this
|
|
171
|
+
existed. So:
|
|
172
|
+
|
|
173
|
+
- **An object is OPEN.** An unlisted property is accepted — TypeScript's
|
|
174
|
+
excess-property check fires on literals only, and a caller who spread a wider object
|
|
175
|
+
is doing something legal.
|
|
176
|
+
- **Anything the compiler could not model becomes `any`** and accepts everything. It
|
|
177
|
+
never guesses a shape from a name, and `Required<T>` compiles to `T` rather than to
|
|
178
|
+
T-with-everything-mandatory, because widening is free and narrowing is not.
|
|
179
|
+
- **Arity is checked DOWNWARD only.** A missing required argument is refused; a surplus
|
|
180
|
+
one is not, because that is what a caller on a version older than a new parameter has.
|
|
181
|
+
- **A union reports the arm the value got FURTHEST into.** `string | { query: string }`
|
|
182
|
+
given `{ query: 5 }` names `args[0].query`, not six arms and no location.
|
|
183
|
+
|
|
184
|
+
The one deliberate strictness is a string literal union (`sort?: "hot" | "new"`), because
|
|
185
|
+
a typo'd enum value is the commonest wrong argument and the accepted set is a legible
|
|
186
|
+
message.
|
|
187
|
+
|
|
188
|
+
**What it fails CLOSED on, and the two things it must not.** A known unit with an
|
|
189
|
+
unknown FUNCTION is refused — the table is authoritative about a unit it carries. An
|
|
190
|
+
unknown UNIT passes straight through, because a Shopify family MEMBER
|
|
191
|
+
(`providers.gymshark`) is absent from every manifest **by design** — `listProviders()`
|
|
192
|
+
excludes members and always will — so refusing an unknown unit would refuse the largest
|
|
193
|
+
part of the library. A function with no readable argument shape is an EXPLICIT `null` in
|
|
194
|
+
the table rather than an absence, and passes through; that distinction is what makes the
|
|
195
|
+
first rule safe at all.
|
|
196
|
+
|
|
197
|
+
**A parameter may not offer a type the wire cannot carry.** Every argument crosses as
|
|
198
|
+
JSON on every surface, so `requestedTime?: string | Date` has an arm refused 100% of the
|
|
199
|
+
time while the library says otherwise. `gate:public-types`' `wire-impossible-param`
|
|
200
|
+
refuses one, with no exception set — an exception would be a declaration that a
|
|
201
|
+
parameter is uncallable. Found once, on `pizzahut.priceOrder`, by generating validators
|
|
202
|
+
for all 253 typed parameters.
|
|
203
|
+
|
|
204
|
+
## Regenerating
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
pnpm run gen:public-types # writes src/generated/{library.d.ts,validators.ts}
|
|
208
|
+
pnpm run gate:public-types # fails if either committed file is stale, or leaks
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**`skipLibCheck: false` in this package's `tsconfig.json` is load-bearing**, and the base
|
|
212
|
+
config sets the opposite. That flag skips type checking of every `.d.ts`, and this
|
|
213
|
+
package's whole deliverable IS a `.d.ts` — with the inherited default, `pnpm typecheck`
|
|
214
|
+
reported green over a generated file carrying 40 unresolved type names. Do not "tidy" it
|
|
215
|
+
back to the inherited value.
|
|
216
|
+
|
|
217
|
+
The file is **committed**, for the reason the tier barrels are: this repo consumes
|
|
218
|
+
TypeScript with no build step, so a generate-at-build artifact leaves `tsc` and vitest
|
|
219
|
+
with nothing to read on a fresh clone. Committed plus a staleness gate is the only shape
|
|
220
|
+
that works. Never hand-edit it.
|
|
221
|
+
|
|
222
|
+
## Four things the generator does that look like bugs and are not
|
|
223
|
+
|
|
224
|
+
**One namespace per unit.** `music` and `flights` both declare `CallOptions`; `Track` and
|
|
225
|
+
`Store` are names any provider may take. Each unit's `types` block is emitted VERBATIM
|
|
226
|
+
inside `declare namespace BowmarkCapability_<id>` / `BowmarkProvider_<id>`, so a signature
|
|
227
|
+
reading `Promise<MusicSearchResult>` resolves against its own block with no rewriting.
|
|
228
|
+
Verbatim is the property that matters: what a caller compiles against is byte-for-byte
|
|
229
|
+
what an agent reads from `get_library`.
|
|
230
|
+
|
|
231
|
+
**The tier is in the namespace name.** `cars` is a capability AND a provider. A single
|
|
232
|
+
`Bowmark_cars` would have emitted one and silently dropped the other.
|
|
233
|
+
|
|
234
|
+
**20 provider functions are deliberately absent**, and none gets a
|
|
235
|
+
`(...args: unknown[])` stand-in — a stand-in compiles, ships, and tells a caller nothing,
|
|
236
|
+
which is the untyped surface this package exists to replace. They stay callable at
|
|
237
|
+
runtime; only the compile-time claim is withheld, because there is no claim to make.
|
|
238
|
+
Each one has a comment in its place saying which class it is and why.
|
|
239
|
+
|
|
240
|
+
- **20 · untyped argument.** The declared argument is a bare destructuring pattern with
|
|
241
|
+
no field types — `findStores({ near, radiusMiles?, limit? })` tells a model everything
|
|
242
|
+
and a compiler nothing.
|
|
243
|
+
- **0 · undeclared type.** The signature names a type the unit's own `types` block never
|
|
244
|
+
declares. This was **11 functions across 7 providers** when the generator first
|
|
245
|
+
compiled a provider's rendered types; all were fixed on 2026-08-05 by copying the real
|
|
246
|
+
interface, and everything it transitively references, into the block. The class is now
|
|
247
|
+
gated at source: `gate:capabilities`' `rendered-types-match-source` runs over the
|
|
248
|
+
provider and family tiers, so it cannot come back silently.
|
|
249
|
+
|
|
250
|
+
`gate:public-types` holds the two lists as separate SETS — merged, a provider could "fix"
|
|
251
|
+
an undeclared type by making the argument untyped and stay green. Both can only shrink.
|
|
252
|
+
|
|
253
|
+
**Seven providers have NO typed surface at all** — `aa`, `dickssportinggoods`,
|
|
254
|
+
`flightradar24`, `ford`, `mailchimp`, `mcdonalds`, `namecheap`. Every function they
|
|
255
|
+
declare is refused, so their interface is empty. The generated file says so in words,
|
|
256
|
+
because an empty interface reads as "this unit does nothing", which is a different and
|
|
257
|
+
wronger claim than "we can make no typed claim about anything it does".
|
|
258
|
+
|
|
259
|
+
## Scale
|
|
260
|
+
|
|
261
|
+
Measured 2026-08-05 on synthetic units, one namespace each:
|
|
262
|
+
|
|
263
|
+
| Units | raw | gzipped | `tsc` | editor cold load | member completion |
|
|
264
|
+
|---|---|---|---|---|---|
|
|
265
|
+
| 10,000 | 6.3 MB | 0.13 MB | 0.7s | 0.4s | 0.2 ms |
|
|
266
|
+
| 500,000 | 321 MB | 6.5 MB | exit 0, 28.3s | 14.5s | **0.4 ms** |
|
|
267
|
+
|
|
268
|
+
Reproduce with `pnpm tsx scripts/bench-public-types.ts <units>`. Completion — the only
|
|
269
|
+
number a person feels — is flat. **The hard ceiling is ~849,000 units**, where the emitted
|
|
270
|
+
string passes V8's `String::kMaxLength`; the generator throws there by name rather than
|
|
271
|
+
letting `Array.prototype.join` produce a bare `RangeError`. Going past it needs the FAMILY
|
|
272
|
+
shape (one shared interface, one line per member), which needs the manifest to describe a
|
|
273
|
+
family and does not today.
|
package/package.json
CHANGED
|
@@ -1 +1,44 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@bowmark/web",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The public client for the Bowmark capability library — real TypeScript for the whole bowmark.* surface, with no Bowmark source on the caller's disk. ZERO runtime dependencies, deliberately and permanently.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://bowmark.ai",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/bowmark-ai/web.git",
|
|
11
|
+
"directory": "node"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/bowmark-ai/web/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"bowmark",
|
|
18
|
+
"agent",
|
|
19
|
+
"capability",
|
|
20
|
+
"browser-automation",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"main": "src/index.ts",
|
|
24
|
+
"types": "src/index.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./src/index.ts"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "~5.9.2"
|
|
43
|
+
}
|
|
44
|
+
}
|