@ultimat3/query 29.4.0 → 30.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/CLAUDE.md CHANGED
@@ -14,6 +14,12 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
14
14
  |---|---|
15
15
  | `query.ts` | the primitive: `query()`, `describeQuery`, `queryHash`; the front door for the read path |
16
16
  | `read.ts` | **the one read path** (`runQuery`, `sourceFor`) + the private declaration store `sql` lives in |
17
+ | `contract-test.ts` | `.contract()`, the generated suite. The three `X_CONTRACT_*` codes and `X_CONTRACT_DRIFT` are borrowed (`QUERY_BORROWED_ERROR_CODES`) |
18
+ | `exercise.ts` | the exercise twin. "Reached" means `sql()` built a source, read off the refusal (`beforeSql`), because `read.ts` keeps no counter. No transaction: a read writes nothing |
19
+ | `declared-errors.ts` | `judgedSql` / `judgedRead`, around `sql` and the source's `execute` in `read.ts`. `errors:` is checked where it is lifted (`facade.ts`), not in `query.ts` |
20
+ | `declared-errors-contract.ts`, `openapi-errors.ts` | `.contract()`'s source scan; core's builder bound to http's status |
21
+ | `http-pin.ts` | a read's pinned `http.path`, its check at `query()`, `servedQueryPath`, `queryHttpPinOf` |
22
+ | `http-pin-error.ts` | `X_QUERY_HTTP_PATH_INVALID`, a leaf over `@ultimat3/core/page` so the browser client can raise it without `errors.ts`'s titles table |
17
23
  | `facade.ts` | the fluent surface — binds each projection to the query, re-implements none |
18
24
  | `http.ts` | route projection (`GET /_x/query/<kebab>`, `enforcedBy: 'handler'`) |
19
25
  | `client.ts` | typed read client (browser-safe); dispatches through `@ultimat3/core`'s `clientTransport`, never `fetch` |
@@ -40,9 +46,10 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
40
46
  | `audit-errors.ts` | `X_QUERY_AUDIT_SINK_MISSING` / `X_QUERY_AUDIT_SINK_FAILED` (titles in `errors.ts`) |
41
47
  | `download.ts` | `query({ download })`: the build, the in-process file, the mint, the serve — through `admittedCall` |
42
48
  | `download-token.ts` | the `_token`: claims `seal()`ed under the master key, opened against one query, one input, the clock |
43
- | `download-http.ts` | the download route: mint JSON without `_token`, the file with it (`Range:`, `content-disposition`) |
49
+ | `download-http.ts` | the download route: mint JSON without `_token`, the file with it (`content-disposition`, and `Range:` by http's `objectResponse`; only `bodyOf` slices a body) |
44
50
  | `download-errors.ts` | the four `X_QUERY_DOWNLOAD_*` classes (titles in `errors.ts`) |
45
51
  | `search-string.ts` | `searchOf`: the input as a search string — the client's request and a minted URL, one encoding |
52
+ | `openapi.ts` | the read half of `openapi.json`; a declared `rows:` is published inline through `toWireOutputSchema`, today's bytes otherwise |
46
53
 
47
54
  ## Invariants — the read path
48
55
 
@@ -70,7 +77,7 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
70
77
  - **A read has ONE tool name, the export name verbatim.** `toToolName` is deleted.
71
78
  `index.test.ts` asserts the barrel exports nothing matching `/tool_?name/i`.
72
79
  - **`queryClient` is the map-wide read client (mirror of `rpc`)**; both spellings run
73
- `queryClientMethodFor`. **`toQueryRoute` and `client()` derive the same URL from core's `queryPath`.**
80
+ `queryClientMethodFor`. **`toQueryRoute` and `client()` derive the same URL from core's `queryPath`, or from the read's pin, which both read from the declaration.**
74
81
  - **`QueryClient` typechecks at any app size, and a `.test.ts` cannot say so** (`tsconfig.json`
75
82
  excludes them): `client-scale-pins.ts` is source, so a regression is a `typecheck` error. The map
76
83
  it pins is the intersection `@ultimat3/action`'s `Merge` hands over; that package pins `Merge`.
@@ -117,6 +124,7 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
117
124
  - **A live read keyed on a sealed column is refused at subscribe** (`assertNoSealedKey`, called by
118
125
  `toLiveQuery`): a change row carries none, so the window would be read once and never patched.
119
126
  The refusal names the column, never the filter's value.
127
+ - **The document's row and the MCP tool's are one narrowing** (`toWireOutputSchema`, `As of 2026-11`). Inline, never a component: the CLI merges only `paths`. Structure only: rows are never re-parsed, so a bound or a closed object is a promise nothing enforces. `openapi.test.ts`, `sealed-egress.test.ts`.
120
128
 
121
129
  ## Invariants — download
122
130
 
@@ -131,6 +139,7 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
131
139
  an expired forgery is a forgery. `X_SEAL_KEY_MISSING` stays a 500 (the server's fault).
132
140
  - **`audit: true` records the mint, `rateLimit:` spends at the mint** — a player's `Range:` GETs
133
141
  are one admitted read.
142
+ - `download-http.ts`: a function `body` is handed the window by `objectResponse`, with `size` required by type and by `assertFile`.
134
143
 
135
144
  ## Invariants — audit
136
145
 
@@ -162,9 +171,9 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
162
171
  - **`@ultimat3/query/client` is the read client without the barrel** — `client.ts`
163
172
  imports core from `@ultimat3/core/page`, the page keys live in the leaf `page-keys.ts`, and
164
173
  `client.ts` never imports `page-controls.ts` or `stable.ts`. `client-bundle.test.ts` fails on any
165
- titles table in the graph or past 12 kB; `client-subpath.test.ts` pins the specifier. Byte figures
166
- (browser, minified): barrel `queryClient` 25,237 B (`As of 2026-10-02`); `./client` 11,983 B
167
- (`As of 2026-10-05`, +971 B: core reads a refusal's `Retry-After` and body title).
174
+ titles table in the graph or past 14.5 kB; `client-subpath.test.ts` pins the specifier. Byte figures
175
+ (browser, minified): barrel `queryClient` 25,237 B (`As of 2026-10-02`); `./client` 14,494 B
176
+ (`As of 2026-11-05`: +1,013 B a pinned read's URL fill, the rest wave 5a's core `./page` additions).
168
177
  - **The record envelope is derived from `rows:`** — `answersRecords(rows)` (`hasEntityRows`,
169
178
  `@ultimat3/entity`), decided once at projection for `http.ts` and `openapi.ts`, sent on every
170
179
  answer. No `rows:` is byte-identical on the wire.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A read. Optionally live. Never a mutation — writes are `action`.
4
4
 
5
+ `bun add @ultimat3/query` · guide: [Queries And Live Queries](https://github.com/developerz-ai/ultimate/wiki/Queries-And-Live-Queries)
6
+
5
7
  ```ts
6
8
  import { query, t } from '@ultimat3/query';
7
9
 
@@ -32,6 +34,7 @@ Every projection is a method on the query itself. A query has no `.def`.
32
34
  | `toolFrom(liveFeed)` | the MCP read tool, from `@ultimat3/mcp` — named `liveFeed`, exactly what `tools/list` serves, reading fresh through `sourceFor` with the same policy object. There is no `liveFeed.tool()` (`As of 25.0.0`): it was a second projection a tier below the served one |
33
35
  | `liveFeed.client({ baseUrl })` | `GET /_x/query/live-feed?orgId=…`, typed both ways. `.page(input, { first, after })` on the same method reads one page over the wire |
34
36
  | `liveFeed.describe()` | the manifest row |
37
+ | `liveFeed.contract({ exercise: { actor } })` | the generated contract suite: the read survives 16 generated legal inputs (`exerciseQuery`), seeded by its name. `exercise` is required unless the policy admits an anonymous reader; `{ off: '<reason>' }` skips it out loud |
35
38
 
36
39
  The route on the other end of that client is `toQueryRoute(liveFeed)`, and — `As of 2026-08` —
37
40
  `x dev` and a container both mount it for every registered read, the framework's job, not the
@@ -53,6 +56,13 @@ const [post] = await queries.publicPost({ slug }); // typed input, typed rows
53
56
 
54
57
  Both spellings run `queryClientMethodFor`, so a read has one URL however it is addressed.
55
58
 
59
+ A read may pin its URL: `query({ …, http: { path: '/orders/:id' } })` is served at `GET /orders/:id`
60
+ in place of `/_x/query/<kebab>`. Each `:param` is a required `t.string` key of `input:`
61
+ (`X_QUERY_HTTP_PATH_INVALID` at `query()`); the URL's value joins the search string before the
62
+ input is parsed, and a search string naming it differently is 400 `X_HTTP_PATH_INPUT_CONFLICT`.
63
+ `read.client()` fills the URL; `queryClient` by name cannot see a pin. `queryHttpPinOf(read)` is
64
+ the manifest's `{ path, method: 'GET' }`.
65
+
56
66
  `queryClient<Api['queries']>` typechecks at any app size: `client-scale-pins.ts` compiles it over
57
67
  100 reads in 50 modules — a list read, its `.page` and a `single: true` read — on every `typecheck`.
58
68
 
@@ -122,6 +132,13 @@ or with a schema no `entity()` branded (a `t.object` look-alike, a `.pick()`ed p
122
132
  is the bare rows, byte-identical to before. `rows:` exists because a read's `sql:` names its table
123
133
  as a STRING, so nothing else carries a schema the envelope could be derived from.
124
134
 
135
+ **Any introspectable `rows:` is also the published row** (`As of 2026-11`), entity or not:
136
+ `openapi.json` and a mount's document print its members inline in the list, the page envelope and
137
+ the single answer, and `x.manifest.json` carries the same wire schema, so `x verify` refuses a
138
+ member removed, retyped or made optional without a major. One narrowing, `toWireOutputSchema`, the
139
+ one the MCP tool's `outputSchema` uses. No `rows:`, or one that cannot be introspected: the bytes
140
+ it had (`items: {}`).
141
+
125
142
  ### Flight control — `clientFlight`, opt-in
126
143
 
127
144
  A typed read is one dispatch and nothing else until a `flight` is installed. With one, N concurrent
@@ -205,7 +222,10 @@ forgetting the policy, and the reason is what tells the next reader which of the
205
222
  | `client.ts` | the typed read client (browser-safe) |
206
223
  | `http.ts` | the route projection — `GET /_x/query/<kebab>`, the URL the client derives |
207
224
  | `page-controls.ts` | `_first` / `_after` — the two search-string keys that page the route, split out and judged at the wire against `@ultimat3/entity`'s `MAX_PAGE_SIZE` |
208
- | `openapi.ts` | the read half of `openapi.json` — one `GET` path item per registered query |
225
+ | `openapi.ts` | the read half of `openapi.json` — one `GET` path item per registered query, publishing a declared `rows:` |
226
+ | `openapi-errors.ts` | a read operation's `responses` with its declared `errors:` in them |
227
+ | `declared-errors.ts` / `declared-errors-contract.ts` | `errors:` judged around `sql` and `execute`; `.contract()`'s scan of `sql`'s source |
228
+ | `contract-test.ts` / `exercise.ts` | `.contract()`: the exercise assertion, a twin of `@ultimat3/action`'s; its three `X_CONTRACT_*` codes are borrowed |
209
229
  | `live.ts` | the `LiveQuery` descriptor `@ultimat3/realtime` subscribes to |
210
230
  | `matcher.ts` | change event → minimal patch (`add` / `update` / `remove` / `refill`) |
211
231
  | `pagination.ts` | `paginate()` — keyset pages over core's cursor codec |
@@ -556,6 +576,13 @@ tier registered with a frozen clock is drivable end to end — and omitting it i
556
576
  for its own default. `@ultimat3/cache`'s tiers refuse a non-positive `ttlMs` and have no immortal
557
577
  entry to offer, so there is no "never" to spell.
558
578
 
579
+ `cache: { negativeTtlMs }` is a `null`'s own lease: a `single: true` read that matched nothing is
580
+ stored as `null` and the stack keeps it `negativeTtlMs`. An empty list is a list, and keeps `ttlMs`.
581
+
582
+ `etag: true` answers `private, no-cache` with a strong `ETag` and a bodiless 304 for a held one —
583
+ `@ultimat3/http`'s `notModified`, after the policy and the rate limit (`http-etag.ts`).
584
+ `timeoutMs` runs the source inside `@ultimat3/db`'s `withStatementTimeout`: always the primary.
585
+
559
586
  The memo entry is the read **in flight**, not its value, so "twice" covers reads that race as
560
587
  well as reads that follow: five holes rendering concurrently share one execution and one tier
561
588
  round trip. A rejection is evicted — a failed read is not the request's answer.
@@ -603,6 +630,14 @@ The conversion is `toBucket` from `@ultimat3/http` — the same one the action r
603
630
  http owns `Bucket` and `action` is the same tier as this package. A pair the limiter cannot run
604
631
  on is `X_RATE_LIMIT_INVALID`, at projection.
605
632
 
633
+ ## `errors:` — the refusals a reader may branch on, closed
634
+
635
+ An action's rule, read side: `query({ errors: [FeedGone.code], … })`, judged around `sql` and the
636
+ source's `execute` (`declared-errors.ts`), scanned in `sql`'s own source by `.contract()`
637
+ (`declared-errors-contract.ts`), named per status in the OpenAPI operation (`openapi-errors.ts`)
638
+ and on the MCP tool's description. Checked where it is lifted (`facade.ts`): sorted, frozen, or
639
+ `X_ACTION_ERRORS_INVALID`. `describe().errors` is the list, `null` when absent.
640
+
606
641
  ## `deprecated:` — a compat window, not a version
607
642
 
608
643
  ```ts
@@ -712,9 +747,9 @@ export const runRecording = query({
712
747
  | Policy | evaluated at MINT, exactly as for a rows read: actor half, the declared `rateLimit:`, the parse, the predicate. A refusal is the policy's own 401/403 |
713
748
  | The token | `seal()`ed (`@ultimat3/core`, AES-256-GCM under the app master key, purpose `ultimate.query.download:<name>`) claims: the query, `fingerprint()` of the PARSED input, the expiry on `ctx.now()`, the minting actor minus `facts`. The ACTOR is ciphertext — no id, role or grant readable; the query's path and its input are plain in the URL, as on any read |
714
749
  | The serve | the seal first (forged, truncated, another query's, a retired key → `X_QUERY_DOWNLOAD_TOKEN_INVALID`, 403), then the expiry (`X_QUERY_DOWNLOAD_TOKEN_EXPIRED`, 410), then the URL's input re-parsed against the bound fingerprint (edited → 403). `handle` runs as the MINTING actor; the policy is not re-run — it decided at mint, with `facts` the token cannot carry |
715
- | `handle`'s answer | `{ body: Uint8Array \| Blob \| ReadableStream, contentType, filename, disposition?: 'attachment' \| 'inline', size? }` — bytes, never base64. Anything else is a 500 (`X_INVARIANT`) naming the fix |
750
+ | `handle`'s answer | `{ body: Uint8Array \| Blob \| ReadableStream, contentType, filename, disposition?: 'attachment' \| 'inline', size? }`, or `body: (range?) => BodyInit` (`DownloadBodyOpener`) with `size` required — bytes, never base64. Anything else is a 500 (`X_INVARIANT`) naming the fix |
716
751
  | Headers | `content-type`, `content-disposition` (`attachment` default; ASCII `filename` + exact `filename*=UTF-8''…`), `x-content-type-options: nosniff`, `content-security-policy: default-src 'none'; img-src 'self'; media-src 'self'; sandbox`, `referrer-policy: no-referrer`, `cache-control: no-store`; `content-length` only when the size is known — a `Uint8Array`, a `Blob`, or a stream with `size` |
717
- | `Range:` | one `bytes=` window, 206 + `content-range`; past the end 416. A `Uint8Array`/`Blob` always (a `Blob` slices lazily); a `ReadableStream` only with `size`, else `accept-ranges: none` and the whole body |
752
+ | `Range:` | one `bytes=` window, 206 + `content-range`; past the end 416. A `Uint8Array`/`Blob` always (a `Blob` slices lazily); a `ReadableStream` only with `size`, else `accept-ranges: none` and the whole body; the function form is handed the window, so `(range) => disk.streamRange(key, [range.start, range.end])` reads nothing before it, and a 304 or a 416 opens nothing |
718
753
  | Rows | `.live()`, `.page()`, `.as()`, `sourceFor`, `explain`, an MCP tool: `X_QUERY_DOWNLOAD_ONLY`. `describeSql` lists it with `sql: null` |
719
754
  | Audit | `audit: true` records the MINT — the decision of who may read what. A serve, and every `Range:` a player sends, is not a second read |
720
755
  | Manifest / OpenAPI | `download: true` on the `QueryFact` (rows ↔ download is breaking); one GET with `_token`, `200` as JSON or `application/octet-stream`, `206`, `410`, `416` |
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@ultimat3/query",
3
- "version": "29.4.0",
3
+ "version": "30.1.0",
4
4
  "description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",
5
+ "keywords": [
6
+ "ultimate",
7
+ "bun",
8
+ "framework",
9
+ "query",
10
+ "live-query",
11
+ "pagination",
12
+ "realtime"
13
+ ],
14
+ "homepage": "https://github.com/developerz-ai/ultimate/wiki/Queries-And-Live-Queries",
5
15
  "license": "MIT",
6
16
  "type": "module",
7
17
  "sideEffects": [
@@ -37,11 +47,12 @@
37
47
  "test": "bun test"
38
48
  },
39
49
  "dependencies": {
40
- "@ultimat3/cache": "29.4.0",
41
- "@ultimat3/core": "29.4.0",
42
- "@ultimat3/entity": "29.4.0",
43
- "@ultimat3/http": "29.4.0",
44
- "@ultimat3/policy": "29.4.0",
45
- "@ultimat3/schema": "29.4.0"
50
+ "@ultimat3/cache": "30.1.0",
51
+ "@ultimat3/core": "30.1.0",
52
+ "@ultimat3/db": "30.1.0",
53
+ "@ultimat3/entity": "30.1.0",
54
+ "@ultimat3/http": "30.1.0",
55
+ "@ultimat3/policy": "30.1.0",
56
+ "@ultimat3/schema": "30.1.0"
46
57
  }
47
58
  }
package/src/cache.ts CHANGED
@@ -199,8 +199,15 @@ export function readThrough<T>(
199
199
  ttlMs: number | null,
200
200
  run: () => Promise<T>,
201
201
  tags: readonly CacheTag[] = [],
202
+ negativeTtlMs: number | null = null,
202
203
  ): Promise<T> {
203
- return readOnce(ctx, key, () => fill(ctx.clock, key, ttlMs, tags, run));
204
+ return readOnce(ctx, key, () => fill(ctx.clock, key, { ttlMs, negativeTtlMs }, tags, run));
205
+ }
206
+
207
+ /** The two leases a filled entry may take: a value's, and a `null`'s when it differs. */
208
+ interface Leases {
209
+ readonly ttlMs: number | null;
210
+ readonly negativeTtlMs: number | null;
204
211
  }
205
212
 
206
213
  /**
@@ -216,11 +223,16 @@ export function readThrough<T>(
216
223
  function fill<T>(
217
224
  clock: Clock,
218
225
  key: string,
219
- ttlMs: number | null,
226
+ { ttlMs, negativeTtlMs }: Leases,
220
227
  tags: readonly CacheTag[],
221
228
  run: () => Promise<T>,
222
229
  ): Promise<T> {
223
230
  // `null` is "the caller named none", never "never": every tier refuses a non-positive `ttlMs`
224
231
  // and none has an immortal entry to offer, so omitting it falls to the tier's own default.
225
- return stackFor(clock).read(key, run, { ...(ttlMs === null ? {} : { ttlMs }), tags });
232
+ // `negativeTtlMs` is the stack's to apply (`ttlOptionsFor`): it alone knows the load was a null.
233
+ return stackFor(clock).read(key, run, {
234
+ ...(ttlMs === null ? {} : { ttlMs }),
235
+ ...(negativeTtlMs === null ? {} : { negativeTtlMs }),
236
+ tags,
237
+ });
226
238
  }
package/src/client.ts CHANGED
@@ -18,9 +18,10 @@
18
18
 
19
19
  import type { ClientFlight, ClientRetry } from '@ultimat3/core';
20
20
  import type { FetchLike, RecordEnvelope } from '@ultimat3/core/page';
21
- import { clientTransport, queryPath } from '@ultimat3/core/page';
21
+ import { clientTransport, fillWire, queryPath, renderedQueryPin } from '@ultimat3/core/page';
22
22
  import type { InferInput, StandardSchemaV1 } from '@ultimat3/schema';
23
23
  import type { DownloadClientMethod, DownloadQuery } from './download';
24
+ import { QueryHttpPathInvalidError } from './http-pin-error';
24
25
  import type { PageControls } from './page-controls';
25
26
  import type { Page } from './pagination';
26
27
  import type { Query } from './query';
@@ -167,8 +168,9 @@ export function queryClient<TQueries extends QueryMap>(
167
168
  export function queryClientMethodFor<TInput extends StandardSchemaV1, TRow extends object>(
168
169
  name: string,
169
170
  options: QueryClientOptions,
171
+ pinnedPath?: string,
170
172
  ): QueryClientMethod<TInput, TRow> {
171
- return methodsOf<TInput, TRow>(name, options);
173
+ return methodsOf<TInput, TRow>(name, options, pinnedPath);
172
174
  }
173
175
 
174
176
  /**
@@ -186,21 +188,33 @@ export function downloadClientFor<TInput extends StandardSchemaV1>(
186
188
  function methodsOf<TInput extends StandardSchemaV1, TRow extends object>(
187
189
  name: string,
188
190
  options: QueryClientOptions,
191
+ pinnedPath?: string,
189
192
  ) {
190
193
  const base = options.baseUrl.replace(/\/+$/, '');
194
+ // A browser caller naming the read only by name — `queryClient`, `useQuery`, a channel's
195
+ // catch-up — goes where the server's document says it pinned it, never to a derived 404.
196
+ const path = pinnedPath ?? renderedQueryPin(name) ?? queryPath(name);
191
197
  // Erased at the wire seam; the row type is this query's by construction.
192
198
  const rows = (input: InferInput<TInput>, callOptions: QueryCallOptions = {}) =>
193
- read(base, options, name, input, callOptions) as Promise<readonly TRow[]>;
199
+ read(base, options, name, input, callOptions, undefined, path) as Promise<readonly TRow[]>;
194
200
  const page = (
195
201
  input: InferInput<TInput>,
196
202
  args: PageControls,
197
203
  callOptions: QueryCallOptions = {},
198
- ) => read(base, options, name, input, callOptions, args) as Promise<Page<TRow>>;
204
+ ) => read(base, options, name, input, callOptions, args, path) as Promise<Page<TRow>>;
199
205
  const url = async (
200
206
  input: InferInput<TInput>,
201
207
  callOptions: QueryCallOptions = {},
202
208
  ): Promise<DownloadUrl> => {
203
- const minted = (await read(base, options, name, input, callOptions)) as DownloadUrl;
209
+ const minted = (await read(
210
+ base,
211
+ options,
212
+ name,
213
+ input,
214
+ callOptions,
215
+ undefined,
216
+ path,
217
+ )) as DownloadUrl;
204
218
  return { ...minted, url: base + minted.url };
205
219
  };
206
220
  return Object.assign(rows, { page, url });
@@ -218,12 +232,22 @@ function read(
218
232
  name: string,
219
233
  input: unknown,
220
234
  callOptions: QueryCallOptions,
221
- page?: PageControls,
235
+ page: PageControls | undefined,
236
+ path: string,
222
237
  ): Promise<unknown> {
223
- const search = searchOf(input, page);
238
+ // The URL carries a `:param` pin's values, each one encoded segment the router decodes back; the
239
+ // search string carries the rest. A missing value is refused before any request exists.
240
+ const filled = fillWire(path, input);
241
+ if (!filled.ok) {
242
+ return Promise.reject(
243
+ new QueryHttpPathInvalidError({ name, path, reason: `:${filled.missing} ${filled.reason}` }),
244
+ );
245
+ }
246
+ const at = filled.url;
247
+ const search = searchOf(filled.rest, page);
224
248
  return clientTransport({
225
249
  method: 'GET',
226
- url: `${base}${queryPath(name)}${search === '' ? '' : `?${search}`}`,
250
+ url: `${base}${at}${search === '' ? '' : `?${search}`}`,
227
251
  // The trace and budget are the transport's, from its server-side outbound slot, placed
228
252
  // before these so an explicit `traceparent` still wins; a browser bundles none of it.
229
253
  ...(options.headers === undefined ? {} : { headers: options.headers }),
@@ -0,0 +1,59 @@
1
+ /**
2
+ * A read's generated contract suite, `orgFeed.contract()`: what the `contract` step runs for a
3
+ * query. Today one assertion — the read survives generated legal input — beside which other
4
+ * claims a read owes (its declared refusals) are appended here, never in a second suite.
5
+ */
6
+
7
+ import type { Ctx } from '@ultimat3/core';
8
+ import { admitsAnonymous } from '@ultimat3/policy';
9
+ import { declaredErrorsContract } from './declared-errors-contract';
10
+ import type { QueryExerciseOptions } from './exercise';
11
+ import { exerciseQuery, queryExerciseTestName, queryExerciseUndeclared } from './exercise';
12
+ import type { AnyQuery } from './query';
13
+ import { queryName } from './read';
14
+
15
+ /** One generated assertion: a name to register under `test()`, and its body. */
16
+ export interface QueryContractTest {
17
+ readonly name: string;
18
+ run(): Promise<void>;
19
+ }
20
+
21
+ export interface QueryContractTestOptions {
22
+ /**
23
+ * Who the generated runs read as — required unless the policy admits an anonymous reader — or
24
+ * `{ off: '<reason>' }`, which skips the runs out loud: the reason is in the test's name.
25
+ */
26
+ readonly exercise?: QueryExerciseOptions;
27
+ /** The context each run starts from; the actor is swapped in, the rest kept. */
28
+ readonly ctx?: Ctx;
29
+ }
30
+
31
+ export function queryContractTestsFor(
32
+ target: AnyQuery,
33
+ options: QueryContractTestOptions = {},
34
+ ): readonly QueryContractTest[] {
35
+ return [exerciseContract(target, options), ...declaredErrorsContract(target)];
36
+ }
37
+
38
+ function exerciseContract(target: AnyQuery, options: QueryContractTestOptions): QueryContractTest {
39
+ const name = queryName(target);
40
+ const exercise = options.exercise;
41
+ if (exercise !== undefined && 'off' in exercise) {
42
+ const reason = exercise.off.trim();
43
+ if (reason.length > 0) {
44
+ return { name: `${name}: read exercise off: ${reason}`, run: () => Promise.resolve() };
45
+ }
46
+ return {
47
+ name: queryExerciseTestName(name),
48
+ run: () => Promise.reject(queryExerciseUndeclared(name, true)),
49
+ };
50
+ }
51
+ const chosen = exercise ?? (admitsAnonymous(target.policy) ? { actor: null } : undefined);
52
+ return {
53
+ name: queryExerciseTestName(name),
54
+ run: async () => {
55
+ if (chosen === undefined) throw queryExerciseUndeclared(name, false);
56
+ await exerciseQuery(target, chosen, options.ctx);
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,34 @@
1
+ // Single responsibility: `.contract()`'s assertion for a query declaring `errors:` — the codes its
2
+ // `sql` names in its own source, judged by core's rule (the action's twin, one tier over). First
3
+ // hop only: what a repository helper throws is the read path's wrap to judge (`declared-errors.ts`).
4
+
5
+ import {
6
+ definedErrorClassCodes,
7
+ ErrorUndeclaredError,
8
+ judgeThrown,
9
+ scanHandlerSource,
10
+ } from '@ultimat3/core';
11
+ import { declaredStatusFor } from '@ultimat3/http';
12
+ import type { QueryContractTest } from './contract-test';
13
+ import type { AnyQuery } from './query';
14
+ import { defOf, queryName } from './read';
15
+
16
+ /** One assertion for a read that declares `errors:`; none for an open contract or a download. */
17
+ export function declaredErrorsContract(target: AnyQuery): readonly QueryContractTest[] {
18
+ const declared = target.errors;
19
+ if (declared === undefined || target.download === true) return [];
20
+ const name = queryName(target);
21
+ return [
22
+ {
23
+ name: `${name}: its read names only the errors it declares`,
24
+ run: async () => {
25
+ const stray = scanHandlerSource(
26
+ defOf(target).sql.toString(),
27
+ await definedErrorClassCodes(),
28
+ ).filter((code) => judgeThrown(declared, code, declaredStatusFor(code)) === 'undeclared');
29
+ if (stray.length === 0) return;
30
+ throw new ErrorUndeclaredError({ primitive: 'query', name }, stray.join("', '"));
31
+ },
32
+ },
33
+ ];
34
+ }
@@ -0,0 +1,26 @@
1
+ // Single responsibility: the judgement the read path puts around `sql` and the source's `execute`
2
+ // for a query declaring `errors:` — core's rule (`declared-errors.ts`, shared with
3
+ // `@ultimat3/action`), `@ultimat3/http`'s status. Undeclared, each call is the read itself.
4
+
5
+ import type { DeclaredErrorsOwner } from '@ultimat3/core';
6
+ import { enforceDeclared, withDeclaredErrors } from '@ultimat3/core';
7
+ import { declaredStatusFor } from '@ultimat3/http';
8
+ import type { AnyQuery } from './query';
9
+
10
+ const ownerOf = (name: string): DeclaredErrorsOwner => ({ primitive: 'query', name });
11
+
12
+ /** `sql(input, ctx)`, which builds a source synchronously: a throw from it is judged here. */
13
+ export function judgedSql<T>(target: AnyQuery, name: string, build: () => T): T {
14
+ if (target.errors === undefined) return build();
15
+ try {
16
+ return build();
17
+ } catch (error) {
18
+ throw enforceDeclared(error, target.errors, declaredStatusFor, ownerOf(name));
19
+ }
20
+ }
21
+
22
+ /** The source's `execute()`: the same promise when undeclared, a judged one otherwise. */
23
+ export function judgedRead<T>(target: AnyQuery, name: string, run: () => Promise<T>): Promise<T> {
24
+ if (target.errors === undefined) return run();
25
+ return withDeclaredErrors(target.errors, declaredStatusFor, ownerOf(name), run);
26
+ }
@@ -9,11 +9,10 @@ import { assert, queryPath, useContext } from '@ultimat3/core';
9
9
  import type { RateLimitDecision, Route, RouteMeta, UltimateRequest } from '@ultimat3/http';
10
10
  import {
11
11
  byteWindow,
12
- contentRangeOf,
13
12
  jsonResponse,
14
- parseByteRange,
13
+ type ObjectRequest,
14
+ objectResponse,
15
15
  publishRateLimit,
16
- RANGE_UNSATISFIABLE,
17
16
  type RequestedRange,
18
17
  } from '@ultimat3/http';
19
18
  import { policyCapability } from '@ultimat3/policy';
@@ -56,7 +55,7 @@ export function toDownloadRoute(
56
55
  return jsonResponse(minted);
57
56
  }
58
57
  const file = await serveDownloadFile(target, input, token, { surface: 'http' });
59
- return fileResponse(file, request.header('range'));
58
+ return fileResponse(request, file);
60
59
  };
61
60
 
62
61
  const meta: RouteMeta = {
@@ -120,10 +119,18 @@ const hasControl = (text: string): boolean =>
120
119
  function assertFile(file: DownloadFile): void {
121
120
  const { body, contentType, filename, disposition, size } = file;
122
121
  assert(
123
- body instanceof Uint8Array || body instanceof Blob || body instanceof ReadableStream,
124
- 'a download handle() answered a body that is not a Uint8Array, a Blob or a ReadableStream',
122
+ body instanceof Uint8Array ||
123
+ body instanceof Blob ||
124
+ body instanceof ReadableStream ||
125
+ typeof body === 'function',
126
+ 'a download handle() answered a body that is not a Uint8Array, a Blob, a ReadableStream or a function',
125
127
  "return { body: bytes, contentType: 'video/mp4', filename: 'run.mp4' } // bytes, never a base64 string",
126
128
  );
129
+ assert(
130
+ typeof body !== 'function' || size !== undefined,
131
+ 'a download handle() answered a function body with no size, so no Range: can be judged',
132
+ "return { body: (range) => open(range), size: object.size, contentType: 'video/mp4', filename: 'run.mp4' }",
133
+ );
127
134
  assert(
128
135
  typeof contentType === 'string' && contentType !== '' && !hasControl(contentType),
129
136
  'a download handle() answered a contentType that is empty, not a string, or carries a control character',
@@ -180,8 +187,9 @@ function bytesBody(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
180
187
  : bytes.slice();
181
188
  }
182
189
 
183
- function bodyOf(file: DownloadFile, range?: RequestedRange): BodyInit {
190
+ function bodyOf(file: DownloadFile, range?: RequestedRange): BodyInit | Promise<BodyInit> {
184
191
  const { body } = file;
192
+ if (typeof body === 'function') return body(range);
185
193
  if (body instanceof Uint8Array) {
186
194
  const whole = bytesBody(body);
187
195
  return range === undefined ? whole : whole.subarray(range.start, range.end + 1);
@@ -192,35 +200,38 @@ function bodyOf(file: DownloadFile, range?: RequestedRange): BodyInit {
192
200
  return range === undefined ? body : byteWindow(body, range);
193
201
  }
194
202
 
195
- /** Status, headers and body for one file and the request's `Range:`, if it named one we serve. */
196
- export function fileResponse(file: DownloadFile, rangeHeader: string | null): Response {
203
+ /**
204
+ * Status, headers and body for one file and the request's `Range:`, by `objectResponse`'s rules.
205
+ * The headers are the file's own and stand in for its sandbox rule. A file of unknown size has no
206
+ * window to serve, so it answers whole; there is no etag on a file either, so no 304.
207
+ */
208
+ export async function fileResponse(request: ObjectRequest, file: DownloadFile): Promise<Response> {
197
209
  assertFile(file);
198
- const headers = new Headers({
199
- 'content-type': file.contentType,
210
+ const own = {
200
211
  'content-disposition': contentDisposition(file),
201
212
  'x-content-type-options': 'nosniff',
202
213
  // Loads nothing but itself, in an opaque origin with no script — and names no URL on the way
203
214
  // out: the one this file is served at is a bearer grant until it expires.
204
215
  'content-security-policy': DOWNLOAD_CSP,
205
216
  'referrer-policy': 'no-referrer',
206
- });
217
+ };
207
218
  const size = sizeOf(file);
208
219
  if (size === undefined) {
209
- headers.set('accept-ranges', 'none');
210
- return new Response(bodyOf(file), { headers });
211
- }
212
- headers.set('accept-ranges', 'bytes');
213
- const range = parseByteRange(rangeHeader, size);
214
- if (range === RANGE_UNSATISFIABLE) {
215
- if (file.body instanceof ReadableStream) void file.body.cancel().catch(() => undefined);
216
- headers.set('content-range', `bytes */${String(size)}`);
217
- return new Response(null, { status: 416, headers });
220
+ return new Response(await bodyOf(file), {
221
+ headers: { 'content-type': file.contentType, 'accept-ranges': 'none', ...own },
222
+ });
218
223
  }
219
- if (range === undefined) {
220
- headers.set('content-length', String(size));
221
- return new Response(bodyOf(file), { headers });
224
+ const response = await objectResponse(
225
+ request,
226
+ {
227
+ object: { contentType: file.contentType, size },
228
+ open: (range) => bodyOf(file, range),
229
+ },
230
+ { headers: own },
231
+ );
232
+ // Nothing opened it, and nothing else will read it: a stream the handler started is let go.
233
+ if (response.status === 416 && file.body instanceof ReadableStream) {
234
+ void file.body.cancel().catch(() => undefined);
222
235
  }
223
- headers.set('content-range', contentRangeOf(range, size));
224
- headers.set('content-length', String(range.end - range.start + 1));
225
- return new Response(bodyOf(file, range), { status: 206, headers });
236
+ return response;
226
237
  }
package/src/download.ts CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type { Ctx, Deprecation } from '@ultimat3/core';
15
15
  import { queryPath, useContext } from '@ultimat3/core';
16
+ import type { RequestedRange } from '@ultimat3/http';
16
17
  import type { InferInput, InferOutput, StandardSchemaV1 } from '@ultimat3/schema';
17
18
  import type { DownloadUrl, QueryCallOptions, QueryClientOptions } from './client';
18
19
  import { downloadClientFor } from './client';
@@ -40,22 +41,40 @@ export const MAX_DOWNLOAD_TTL_MS = 3_600_000;
40
41
  const TOKEN_IS_THE_GRANT =
41
42
  'a download token is sealed only by url(), after this query admitted the minting actor; the token is that decision';
42
43
 
43
- /** What `handle` answers. Bytes, never a base64 string — that is the +33 % this exists to remove. */
44
- export interface DownloadFile {
45
- /**
46
- * A `Uint8Array` or a `Blob` (a `Bun.file`, a slice) is range-served from its own length; a
47
- * `ReadableStream` is range-served only with `size`, else whole with `accept-ranges: none`.
48
- */
49
- readonly body: Uint8Array | Blob | ReadableStream<Uint8Array>;
44
+ interface DownloadFileHead {
50
45
  readonly contentType: string;
51
46
  /** The name a browser saves it under — `content-disposition`'s `filename` and `filename*`. */
52
47
  readonly filename: string;
53
48
  /** `attachment` (default) saves it; `inline` lets a tab render it. `<video>` ignores both. */
54
49
  readonly disposition?: 'attachment' | 'inline' | undefined;
55
- /** The stream's length in bytes. Ignored for a `Uint8Array` or a `Blob`, which know theirs. */
56
- readonly size?: number | undefined;
57
50
  }
58
51
 
52
+ /**
53
+ * Opens the bytes asked for: the window with a `Range:`, the whole file with none, and not at all
54
+ * for a 304 or a 416. So a stored object's seek reads only its window, with no `storage` edge here:
55
+ * `(range) => range === undefined ? disk.stream(key) : disk.streamRange(key, [range.start, range.end])`.
56
+ */
57
+ export type DownloadBodyOpener = (range?: RequestedRange) => BodyInit | Promise<BodyInit>;
58
+
59
+ /** What `handle` answers. Bytes, never a base64 string — that is the +33 % this exists to remove. */
60
+ export type DownloadFile =
61
+ | (DownloadFileHead & {
62
+ /**
63
+ * A `Uint8Array` or a `Blob` (a `Bun.file`, a slice) is range-served from its own length; a
64
+ * `ReadableStream` is range-served only with `size`, else whole with `accept-ranges: none`,
65
+ * and a window of one has read and dropped everything before it.
66
+ */
67
+ readonly body: Uint8Array | Blob | ReadableStream<Uint8Array>;
68
+ /** The stream's length in bytes. Ignored for a `Uint8Array` or a `Blob`, which know theirs. */
69
+ readonly size?: number | undefined;
70
+ })
71
+ | (DownloadFileHead & {
72
+ /** The disk's own window. A sealed object cannot take it: an AES-GCM envelope opens whole. */
73
+ readonly body: DownloadBodyOpener;
74
+ /** Required: it is what a `Range:` is judged against before anything is opened. */
75
+ readonly size: number;
76
+ });
77
+
59
78
  export interface DownloadArgs<TInput extends StandardSchemaV1> {
60
79
  readonly input: InferOutput<TInput>;
61
80
  readonly ctx: Ctx;