@ultimat3/query 24.0.0 → 25.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.
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The only file in this package that calls an `AuditSink`: `query({ audit: true })` records every
3
+ * call — allowed, denied, failed — into the one sink `@ultimat3/core` holds, in the record shape
4
+ * `@ultimat3/action` writes. It observes the read path; it never forks it.
5
+ */
6
+
7
+ import type { AuditOutcome, AuditRecord, AuditSink, Ctx } from '@ultimat3/core';
8
+ import { getAuditSink, isUltimateError, logger } from '@ultimat3/core';
9
+ import { QueryAuditSinkFailedError, QueryAuditSinkMissingError } from './audit-errors';
10
+ import { QueryDeniedError } from './errors';
11
+ import type { QuerySurface } from './policy-gate';
12
+ import type { SqlSource } from './source';
13
+
14
+ /**
15
+ * What the read path learns on the way through, for the record — the same two facts `invoke`'s
16
+ * trace carries for an action, minus the idempotency key a read never has. Mutable on purpose:
17
+ * `buildSource` fills `input`, the memo fills `replayed`, and reading them back out here is what
18
+ * keeps the audited branch from becoming a second read path.
19
+ */
20
+ export interface ReadTrace {
21
+ input: unknown;
22
+ replayed: boolean;
23
+ }
24
+
25
+ /** One audited call, resolved before anything is read: the sink, and what every record shares. */
26
+ export interface ReadAudit {
27
+ readonly sink: AuditSink;
28
+ readonly name: string;
29
+ readonly ctx: Ctx;
30
+ readonly surface: QuerySurface;
31
+ readonly trace: ReadTrace;
32
+ }
33
+
34
+ /**
35
+ * `null` when the read is not audited — not declared, or built `unenforced`, which has no caller
36
+ * to attribute a sighting to (`explain`, the shared live window). Otherwise the sink, or
37
+ * `X_QUERY_AUDIT_SINK_MISSING` BEFORE the input parse: an audited read nothing can record refuses
38
+ * while it has still read nothing.
39
+ */
40
+ export function readAuditFor(
41
+ audit: boolean | undefined,
42
+ unenforced: string | undefined,
43
+ name: string,
44
+ ctx: Ctx,
45
+ surface: QuerySurface,
46
+ ): ReadAudit | null {
47
+ if (audit !== true || unenforced !== undefined) return null;
48
+ const sink = getAuditSink();
49
+ if (sink === null) throw new QueryAuditSinkMissingError(name);
50
+ return { sink, name, ctx, surface, trace: { input: undefined, replayed: false } };
51
+ }
52
+
53
+ /**
54
+ * One call, one record. A memo or cache hit is recorded like a miss — with `replayed: true` — and
55
+ * that is the point of the option rather than an accident of where it sits: an audit of a read is
56
+ * about WHO SAW WHAT, and a reader handed rows from the request memo saw them exactly as much as
57
+ * the reader whose call executed the SQL. Recording only executions would answer "what did the
58
+ * database compute", which no auditor asks.
59
+ *
60
+ * The failure policies are `@ultimat3/action`'s: a refused record of a denied or failed read is
61
+ * logged and the original error still reaches the caller (replacing an `X_FORBIDDEN` would turn
62
+ * the audit backend's health into an oracle); a refused record of an ALLOWED read withholds the
63
+ * rows (`X_QUERY_AUDIT_SINK_FAILED`) — a read commits nothing, so refusing costs one retry.
64
+ */
65
+ export async function audited<T>(audit: ReadAudit, run: () => Promise<T>): Promise<T> {
66
+ // When the attempt began, from the context's clock — never `new Date()`.
67
+ const at = audit.ctx.now();
68
+ let value: T;
69
+ try {
70
+ value = await run();
71
+ } catch (error) {
72
+ await recordThrew(audit.sink, recordOf(audit, at, readOutcomeFor(error), error));
73
+ throw error;
74
+ }
75
+ // Outside the `catch` on purpose, as in `invoke`: a refused record here describes the RECORD,
76
+ // and letting it fall into that branch would write a second record claiming the read failed.
77
+ await recordSettled(audit.sink, recordOf(audit, at, 'allowed', undefined));
78
+ return value;
79
+ }
80
+
81
+ /**
82
+ * The public `sourceFor`'s half: a build that refuses (denied, unparsed, rate-limited) is a call
83
+ * that ended, recorded now because no `execute()` will follow; a build that succeeds records
84
+ * nothing yet and hands back a source whose every execution is recorded.
85
+ */
86
+ export async function auditedBuild<TRow>(
87
+ audit: ReadAudit,
88
+ build: () => Promise<SqlSource<TRow>>,
89
+ ): Promise<SqlSource<TRow>> {
90
+ const at = audit.ctx.now();
91
+ let source: SqlSource<TRow>;
92
+ try {
93
+ source = await build();
94
+ } catch (error) {
95
+ await recordThrew(audit.sink, recordOf(audit, at, readOutcomeFor(error), error));
96
+ throw error;
97
+ }
98
+ return auditedSource(source, audit);
99
+ }
100
+
101
+ /**
102
+ * A source handed OUT of this package (`sourceFor`) for an audited read: every `execute()` is one
103
+ * call and one record — `@ultimat3/mcp`'s served tool builds through `sourceFor` and executes once,
104
+ * so its reads are recorded under `surface: 'mcp'` without a second path. `seek()` and `total()`
105
+ * answer audited sources too, since `paginate` and a live build execute what those return.
106
+ */
107
+ export function auditedSource<TRow>(source: SqlSource<TRow>, audit: ReadAudit): SqlSource<TRow> {
108
+ // Each execution is its own call: a fresh trace, so one call's facts never leak into the next.
109
+ const call = (): ReadAudit => ({ ...audit, trace: { ...audit.trace, replayed: false } });
110
+ const { total, seek } = source;
111
+ return {
112
+ toSQL: () => source.toSQL(),
113
+ shape: () => source.shape(),
114
+ execute: () => audited(call(), () => source.execute()),
115
+ ...(total === undefined ? {} : { total: () => auditedSource(total.call(source), audit) }),
116
+ ...(seek === undefined
117
+ ? {}
118
+ : { seek: (after, limit) => auditedSource(seek.call(source, after, limit), audit) }),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * An authz refusal is `denied`; everything else that threw is `failed`, an unparsed input and a
124
+ * spent rate limit included — the same split `@ultimat3/action`'s `auditOutcomeFor` makes, and
125
+ * TOTAL for the same reason: `instanceof` runs a `Proxy`'s trap, and a probe that threw inside
126
+ * this `catch` would replace the caller's error with its own.
127
+ */
128
+ export function readOutcomeFor(error: unknown): AuditOutcome {
129
+ try {
130
+ return error instanceof QueryDeniedError ? 'denied' : 'failed';
131
+ } catch {
132
+ return 'failed';
133
+ }
134
+ }
135
+
136
+ function recordOf(audit: ReadAudit, at: Date, outcome: AuditOutcome, error: unknown): AuditRecord {
137
+ return {
138
+ at,
139
+ name: audit.name,
140
+ primitive: 'query',
141
+ mutator: false,
142
+ surface: audit.surface,
143
+ ctx: audit.ctx,
144
+ // The PARSED input, exactly as an action's record carries it — never the raw payload, and
145
+ // never the rows: a sink that persists redacts through `auditableInput`, the walk core's
146
+ // credential table and `Secret` decide, and a result is absent from the record by design.
147
+ input: audit.trace.input,
148
+ idempotencyKey: null,
149
+ replayed: audit.trace.replayed,
150
+ outcome,
151
+ failure:
152
+ outcome === 'allowed' ? null : { code: isUltimateError(error) ? error.code : null, error },
153
+ };
154
+ }
155
+
156
+ async function recordSettled(sink: AuditSink, record: AuditRecord): Promise<void> {
157
+ try {
158
+ await sink.write(record);
159
+ } catch (error) {
160
+ throw new QueryAuditSinkFailedError(record.name, error);
161
+ }
162
+ }
163
+
164
+ async function recordThrew(sink: AuditSink, record: AuditRecord): Promise<void> {
165
+ try {
166
+ await sink.write(record);
167
+ } catch (error) {
168
+ // The same line `@ultimat3/action` writes, so `audit.sink.failed` is one alert rule over every
169
+ // audited primitive. Never the record: rendering an input the sink just choked on is the
170
+ // second throw this branch exists to prevent.
171
+ logger.error('audit.sink.failed', {
172
+ query: record.name,
173
+ outcome: record.outcome,
174
+ error,
175
+ });
176
+ }
177
+ }
package/src/cache.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { CacheStack, CacheTag, CacheTier } from '@ultimat3/cache';
9
- import { createCacheStack, registeredTiers, tagKeys } from '@ultimat3/cache';
9
+ import { cacheStack, registeredTiers, tagKeys } from '@ultimat3/cache';
10
10
  import type { Actor, Clock, Ctx } from '@ultimat3/core';
11
11
  import { assertNever, fingerprint } from '@ultimat3/core';
12
12
 
@@ -164,7 +164,7 @@ async function publish<T>(
164
164
  /**
165
165
  * One stack per (registry, clock) — never one per read.
166
166
  *
167
- * `createCacheStack` owns a single-flight map, so a stack built per call joins nothing and the
167
+ * `cacheStack` owns a single-flight map, so a stack built per call joins nothing and the
168
168
  * cross-request stampede guard would be a no-op. Keyed on the clock because the stack's expiry
169
169
  * decision and the tiers' own have to agree: a tier registered with a frozen clock under a stack
170
170
  * reading the wall clock calls every entry expired, which is the shape that made the old read
@@ -182,7 +182,7 @@ function stackFor(clock: Clock): CacheStack {
182
182
  // read, and `resetTiers()` between suites. Compared element-wise by identity: a tier object is
183
183
  // registered once and never mutated, so two equal lists are the same ladder.
184
184
  if (held !== undefined && sameTiers(held.tiers, tiers)) return held.stack;
185
- const stack = createCacheStack(tiers, { clock });
185
+ const stack = cacheStack(tiers, { clock });
186
186
  stacks.set(clock, { tiers, stack });
187
187
  return stack;
188
188
  }
@@ -208,7 +208,7 @@ export function readThrough<T>(
208
208
  * package's own. Runs once per key per request; the rest join it at the memo above.
209
209
  *
210
210
  * Everything this used to do by hand — the fence sampled before the load, `bestEffort` around
211
- * every tier call, the expiry — is `createCacheStack`'s, which is the point: there was one read
211
+ * every tier call, the expiry — is `cacheStack`'s, which is the point: there was one read
212
212
  * cache too many, and the one that lived here was in no registry, so `invalidateTags` could not
213
213
  * reach it. A relative `ttlMs` and never an absolute expiry: the tier's own clock decides when
214
214
  * the entry dies, so a tier registered with a frozen clock is drivable end to end.
package/src/client.ts CHANGED
@@ -11,15 +11,14 @@
11
11
  * transport, its rows adopted into the page's store, so the return type is the rows either way.
12
12
  *
13
13
  * `ClientFlight` is a TYPE here and never a value: dedup, retry, the deadline and the fence are
14
- * `@ultimat3/core`'s `client-flight.ts`, and a caller that never calls `createClientFlight` does
14
+ * `@ultimat3/core`'s `client-flight.ts`, and a caller that never calls `clientFlight` does
15
15
  * not pay a byte for any of them — an `import type` is erased and the value import would not be.
16
16
  */
17
17
 
18
18
  import type { ClientFlight, ClientRetry } from '@ultimat3/core';
19
19
  import type { FetchLike, RecordEnvelope } from '@ultimat3/core/page';
20
- import { clientTransport, isJsonObject } from '@ultimat3/core/page';
20
+ import { clientTransport, isJsonObject, queryPath } from '@ultimat3/core/page';
21
21
  import type { InferInput, StandardSchemaV1 } from '@ultimat3/schema';
22
- import { derivePath } from './naming';
23
22
  import type { PageControls } from './page-controls';
24
23
  import { PAGE_AFTER_KEY, PAGE_FIRST_KEY } from './page-keys';
25
24
  import type { Page } from './pagination';
@@ -33,7 +32,7 @@ export interface QueryClientOptions {
33
32
  readonly fetch?: FetchLike;
34
33
  readonly headers?: Readonly<Record<string, string>>;
35
34
  /**
36
- * Opt-in flight control for every read this client makes — `createClientFlight({ principal })`.
35
+ * Opt-in flight control for every read this client makes — `clientFlight({ principal })`.
37
36
  * Absent, a read is one dispatch and nothing else, which is what every caller written before
38
37
  * this option existed already gets.
39
38
  */
@@ -183,7 +182,7 @@ function read(
183
182
  const search = searchOf(input, page);
184
183
  return clientTransport({
185
184
  method: 'GET',
186
- url: `${base}${derivePath(name)}${search === '' ? '' : `?${search}`}`,
185
+ url: `${base}${queryPath(name)}${search === '' ? '' : `?${search}`}`,
187
186
  // The trace and budget are the transport's, from its server-side outbound slot, placed
188
187
  // before these so an explicit `traceparent` still wins; a browser bundles none of it.
189
188
  ...(options.headers === undefined ? {} : { headers: options.headers }),
package/src/errors.ts CHANGED
@@ -9,8 +9,8 @@ import type { SurfaceDenial } from '@ultimat3/policy';
9
9
  // no anchor. The `https://ultimate.dev/errors/<code>` links this file built until 9.x answered
10
10
  // 404, host included, on every read this package has ever refused.
11
11
 
12
- /** One class, one code: core owns the cursor codec, so core owns `X_CURSOR_INVALID`. */
13
- export { CursorInvalidError } from '@ultimat3/core';
12
+ // `CursorInvalidError` is core's and imported from there: core owns the cursor codec, so core
13
+ // owns `X_CURSOR_INVALID` and its one import path.
14
14
 
15
15
  /** Titles for the framework-wide code table — every one of them owned by this package. */
16
16
  const OWNED_TITLES: Readonly<Record<string, string>> = {
@@ -28,6 +28,9 @@ const OWNED_TITLES: Readonly<Record<string, string>> = {
28
28
  X_QUERY_SUBSCRIBES_INVALID: 'a query declares subscribed relations no live read can use',
29
29
  X_QUERY_UNREGISTERED: 'a query was used before it was registered',
30
30
  X_QUERY_SINGLE_INVALID: 'a query declares single: as something other than a boolean',
31
+ X_QUERY_AUDIT_SINK_MISSING: 'a query declares audit: true and no audit sink is installed',
32
+ X_QUERY_AUDIT_SINK_FAILED:
33
+ 'an audited read was refused because the audit sink rejected its record',
31
34
  };
32
35
 
33
36
  /**
@@ -74,7 +77,7 @@ function denialReason(denial: SurfaceDenial): string {
74
77
  }
75
78
 
76
79
  /**
77
- * An authz denial from `guard()`. The code and reason come from the policy
80
+ * An authz denial from `guardQuery()`. The code and reason come from the policy
78
81
  * decision — this package never invents an authz code — and the surface-shaped
79
82
  * denial rides along so a live socket can close with 4403 rather than a 403 body.
80
83
  */
package/src/facade.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  /**
2
- * The fluent surface: every projection reachable as a method on the query itself,
3
- * `orgFeed.tool()` rather than `toQueryTool(orgFeed)`, and every declared field
4
- * lifted off `def` so app code never reaches through `.def`. The projection
5
- * functions stay exported for the framework's own call sites — this file only
6
- * binds them to the query, it never re-implements one.
2
+ * The fluent surface: every projection this package owns reachable as a method on the query
3
+ * itself, `orgFeed.live(input)` rather than `toLiveQuery(orgFeed, input)`, and every declared
4
+ * field lifted off `def` so app code never reaches through `.def`. The projection functions stay
5
+ * exported for the framework's own call sites — this file only binds them to the query, it never
6
+ * re-implements one. There is no `.tool()`: the MCP tool is `@ultimat3/mcp`'s one projection
7
+ * (`toolFrom(orgFeed)`), a tier this package cannot import.
7
8
  */
8
9
 
9
10
  import type { StandardSchemaV1 } from '@ultimat3/schema';
10
11
  import type { QueryClientMethodOf } from './client';
11
12
  import { queryClientMethodFor } from './client';
12
13
  import { toLiveQuery } from './live';
13
- import { toQueryTool } from './mcp-tool';
14
14
  import { paginate } from './pagination';
15
15
  import type { Query, QueryDef, QueryFacade } from './query';
16
16
  import { queryName, runQuery } from './read';
@@ -45,7 +45,6 @@ export function facadeFor<
45
45
  // signed cursor is only reachable through the query that issued it.
46
46
  page: (input, args) => paginate(self(), input, args),
47
47
  live: (input, options) => toLiveQuery(self(), input, options),
48
- tool: () => toQueryTool<TSingle>(self()),
49
48
  // One implementation for both wire shapes: the route decides the body, the transport parses
50
49
  // it, and only the TYPE differs — a single read's method answers the row, and has no `page`.
51
50
  client: (options) =>
package/src/http.ts CHANGED
@@ -6,17 +6,16 @@
6
6
  */
7
7
 
8
8
  import { tagKeys } from '@ultimat3/cache';
9
- import type { Route, RouteMeta, UltimateRequest } from '@ultimat3/http';
10
- import { toBucket } from '@ultimat3/http';
9
+ import type { Deprecation } from '@ultimat3/core';
10
+ import { queryPath, recordDeprecatedCall, renderDeprecation, useContext } from '@ultimat3/core';
11
+ import type { RateLimitDecision, Route, RouteMeta, UltimateRequest } from '@ultimat3/http';
12
+ import { publishRateLimit, toBucket } from '@ultimat3/http';
13
+ import { admitsAnonymous, policyCapability } from '@ultimat3/policy';
11
14
  import { coerceQuery } from '@ultimat3/schema';
12
- import type { Deprecation } from './deprecation';
13
- import { recordDeprecatedCall, renderDeprecation } from './deprecation';
14
15
  import { QueryDeprecationInvalidError, QueryInputInvalidError } from './errors';
15
16
  import { absentArraysOf } from './input-shape';
16
- import { derivePath } from './naming';
17
17
  import { PAGE_FIRST_KEY, pageControlsOf } from './page-controls';
18
- import { admitsAnonymous, policyCapability } from './policy-gate';
19
- import type { AnyQuery } from './query';
18
+ import type { AnyQuery, QueryRateLimit } from './query';
20
19
  import { queryName, runQuery } from './read';
21
20
  import { recordAnswerFor, recordRowAnswerFor } from './record-answer';
22
21
  import { oneRowOf } from './single-answer';
@@ -24,8 +23,7 @@ import { oneRowOf } from './single-answer';
24
23
  /**
25
24
  * `liveFeed` -> `GET /_x/query/live-feed`. Named for the primitive rather than spelled
26
25
  * `toRoute`, because a host mounts this beside `@ultimat3/action`'s and an alias at the
27
- * import site is a name the reader has to hold — the same reason the tool projection
28
- * here is `toQueryTool`.
26
+ * import site is a name the reader has to hold.
29
27
  */
30
28
  export function toQueryRoute(target: AnyQuery): Route {
31
29
  const name = queryName(target);
@@ -60,6 +58,14 @@ export function toQueryRoute(target: AnyQuery): Route {
60
58
  //
61
59
  // The two page controls come OUT first (`page-controls.ts`): they are the route's, not the
62
60
  // read's, and a schema that refused unknown keys would otherwise refuse every paged call.
61
+ // The read spends its declared limit itself (`spendReadLimit`, on every surface), keyed on
62
+ // this request's address for an anonymous reader, and hands the decision back for the headers.
63
+ const spending = {
64
+ surface: 'http',
65
+ clientAddress: request.ctx.ip,
66
+ onRateLimit: (decision: RateLimitDecision) =>
67
+ publishRateLimit(request.ctx, decision, useContext().now().getTime()),
68
+ } as const;
63
69
  const { input: values, page } = pageControlsOf(name, request.queryRaw());
64
70
  const input = absentArraysOf(target.input, coerceQuery(target.input, values));
65
71
  if (target.single === true) {
@@ -73,19 +79,18 @@ export function toQueryRoute(target: AnyQuery): Route {
73
79
  }
74
80
  // The first row — what every in-process `[0]` of the same read already takes. None is the
75
81
  // 404 a detail URL means, where a list read answers `200 []`.
76
- return answerRow(oneRowOf(name, await runQuery(target, input, { surface: 'http' })));
82
+ return answerRow(oneRowOf(name, await runQuery(target, input, spending)));
77
83
  }
78
84
  // With a page control the answer is the `Page` envelope `query.page()` answers a server
79
- // caller with — `{ rows, nextCursor, hasMore, endCursor, hasNextPage }`, the same names (the
80
- // last two aliases of the first two), so a cursor read off the
85
+ // caller with — `{ rows, nextCursor, hasMore }`, the same names, so a cursor read off the
81
86
  // wire and one read off a direct call are the same string in the same field. Without one the
82
87
  // answer is the bare array it has always been: every client written before the controls
83
- // existed keeps reading rows, and `hasNextPage` never rides on a row where a page marker
84
- // has no business being.
88
+ // existed keeps reading rows, and `hasMore` never rides on a row where a page marker has no
89
+ // business being.
85
90
  return answer(
86
91
  page === undefined
87
- ? await runQuery(target, input, { surface: 'http' })
88
- : await target.page(input, { ...page, surface: 'http' }),
92
+ ? await runQuery(target, input, spending)
93
+ : await target.page(input, { ...page, ...spending }),
89
94
  );
90
95
  };
91
96
 
@@ -102,6 +107,9 @@ export function toQueryRoute(target: AnyQuery): Route {
102
107
  // rule reads (`ownsOrg(actor, input.orgId)`); the stage would decide the same policy
103
108
  // from raw strings, and would need an `authorize` hook wired to decide at all.
104
109
  enforcedBy: 'handler',
110
+ // Which primitive this is, for `@ultimat3/http`'s bucket check (a bucket named after a query
111
+ // limits nothing since 25.0.0, and is refused rather than ignored).
112
+ primitive: 'query',
105
113
  // `input` stays absent, deliberately: the pipeline's body stage validates `meta.input`
106
114
  // against the BODY, and a GET has none — declaring it would fail every read on an
107
115
  // absent body before the handler ran. The schema is not skipped, it is applied in the
@@ -114,23 +122,29 @@ export function toQueryRoute(target: AnyQuery): Route {
114
122
  // ride along so a purge can still name the read the tier keys by.
115
123
  cache: { mode: 'no-store', tags: tagKeys(target.cache?.tags ?? []) },
116
124
  tags: ['query'],
117
- // Name AND numbers, exactly as an action's route sets them — the name alone selects a bucket
118
- // the limiter's table never held, so `bucketFor` falls through to `default` (120 burst, 2/s)
119
- // and a read declaring 5 runs on 120. `withRouteBuckets` registers the pair at construction.
120
- // `toBucket` is `@ultimat3/http`'s: the limiter owns the maths, and a copy here would be a
121
- // second conversion able to publish numbers the limiter refuses.
125
+ // The read spends its own declared bucket on every surface (`read.ts`), so the stage spends
126
+ // no caller bucket here: not this one (the old HTTP-only enforcement point the MCP tool never
127
+ // reached), and not `default` (a ceiling a read declaring more than it never had). The tenant
128
+ // allowance stays the stage's. Refused at mount, never on the first read, when the pair is one
129
+ // the limiter cannot run on: `toBucket` is `@ultimat3/http`'s, so the numbers cannot drift.
122
130
  ...(target.rateLimit === undefined
123
131
  ? {}
124
- : { rateLimit: name, rateLimitBucket: toBucket(name, target.rateLimit) }),
132
+ : { rateLimitedBy: rateLimitedByHandler(name, target.rateLimit) }),
125
133
  ...(target.mcp?.description === undefined ? {} : { description: target.mcp.description }),
126
134
  };
127
135
 
128
- return { method: 'GET', path: derivePath(name), handler, meta };
136
+ return { method: 'GET', path: queryPath(name), handler, meta };
137
+ }
138
+
139
+ /** Validates the declaration through the limiter's own conversion, then hands the route its flag. */
140
+ function rateLimitedByHandler(name: string, limit: QueryRateLimit): 'handler' {
141
+ toBucket(name, limit);
142
+ return 'handler';
129
143
  }
130
144
 
131
145
  /**
132
146
  * The headers this read's `deprecated:` block renders to, or nothing. The successor's URL comes
133
- * from `derivePath` — the same derivation `client()` uses, never a second one.
147
+ * from `queryPath` — the same derivation `client()` uses, never a second one.
134
148
  */
135
149
  function deprecationHeadersFor(
136
150
  name: string,
@@ -138,7 +152,7 @@ function deprecationHeadersFor(
138
152
  ): Readonly<Record<string, string>> | undefined {
139
153
  if (deprecated === undefined) return undefined;
140
154
  const successor =
141
- deprecated.replacedBy === undefined ? undefined : derivePath(deprecated.replacedBy);
155
+ deprecated.replacedBy === undefined ? undefined : queryPath(deprecated.replacedBy);
142
156
  const rendered = renderDeprecation(deprecated, successor);
143
157
  if (!rendered.ok) throw new QueryDeprecationInvalidError(name, rendered.field, rendered.value);
144
158
  return rendered.headers;
package/src/index.ts CHANGED
@@ -6,8 +6,10 @@
6
6
  * authorize or execute on its own. One authz system, structurally.
7
7
  */
8
8
 
9
- // Anchored on purpose, and NOT by the `sideEffects` array: Bun reads any array as `false` and drops
10
- // the module regardless (oven-sh/bun#40650). `registerPrimitiveRegistrar('query', …)` here is read
9
+ // Anchored on purpose, and not by the `sideEffects` array alone: Bun before 1.4.1 read any array as
10
+ // `false` and dropped the module regardless (oven-sh/bun#40650), and a bare import holds on every
11
+ // bundler. The array still lists it, because a bundler that honours the array drops a bare import
12
+ // of a module it does not list. `registerPrimitiveRegistrar('query', …)` here is read
11
13
  // by @ultimat3/core's registrar table on behalf of `x` and the manifest, and nothing that registers
12
14
  // a query imports this module for a binding. `SIDE_EFFECTS_ANCHORS` carries the argument and
13
15
  // `bun run side-effects` enforces it.
@@ -15,33 +17,25 @@ import './registry';
15
17
 
16
18
  /**
17
19
  * Flight control for the typed client, and OPT-IN by construction: `client.ts` names `ClientFlight`
18
- * as a TYPE only, so a caller that never mentions `createClientFlight` pays nothing for the fence,
20
+ * as a TYPE only, so a caller that never mentions `clientFlight` pays nothing for the fence,
19
21
  * the dedup map or the retry loop. Every mechanism underneath is `@ultimat3/core`'s — one fence,
20
22
  * one flight map, one gate, one backoff curve for the whole framework — and so is the pipeline
21
23
  * itself: it shipped as a byte-identical copy here and in `@ultimat3/action`, and
22
24
  * two tier-3 packages may not import each other, so the one copy lives at tier 0.
23
25
  *
24
- * Re-exported rather than re-declared, so every name is importable from this package exactly as
25
- * before. `isSuperseded` is core's too: reading a fenced answer is the point of installing a
26
- * flight, and it should not cost a second import.
26
+ * `ClientFlight` and `ClientRetry` are re-exported as TYPES because this barrel's options name
27
+ * them; every value (`clientFlight`, `isSuperseded`, …) is imported from `@ultimat3/core`,
28
+ * its one home — a re-export is a second import path (`X_HELPER_COPY`, `bun run flight-copies`).
27
29
  */
28
- export type {
29
- ClientFlight,
30
- ClientFlightOptions,
31
- ClientRetry,
32
- FlightKeyOptions,
33
- FlightPlan,
34
- WireAnswer,
35
- } from '@ultimat3/core';
36
- export {
37
- createClientFlight,
38
- DEFAULT_CLIENT_RETRY,
39
- isSuperseded,
40
- isTransientFailure,
41
- } from '@ultimat3/core';
30
+ export type { ClientFlight, ClientRetry } from '@ultimat3/core';
42
31
  /** Re-exported so a `query` file needs one import, not two. Same object as schema's. */
43
32
  export type { Infer } from '@ultimat3/schema';
44
33
  export { t } from '@ultimat3/schema';
34
+ /**
35
+ * The two refusals `query({ audit: true })` adds. The record and the sink are `@ultimat3/core`'s
36
+ * (`setAuditSink`) — one sink for every audited primitive, so this package re-exports neither.
37
+ */
38
+ export { QueryAuditSinkFailedError, QueryAuditSinkMissingError } from './audit-errors';
45
39
  export type { QueryCacheScope } from './cache';
46
40
  /** `readAuthority` is the ONLY producer of `cacheKeyFor`'s authority — never spell one by hand. */
47
41
  export {
@@ -73,11 +67,7 @@ export { queryClient, queryClientMethodFor } from './client';
73
67
  */
74
68
  export type { KindOf } from './column-kinds';
75
69
  export { kindsOf } from './column-kinds';
76
- /** The compat window a retirement gets. Versioning is two deployments, not a router feature. */
77
- export type { Deprecation, DeprecationField, DeprecationRender } from './deprecation';
78
- export { recordDeprecatedCall, renderDeprecation } from './deprecation';
79
70
  export {
80
- CursorInvalidError,
81
71
  CursorValueUnsupportedError,
82
72
  MatcherUnsupportedError,
83
73
  QueryColumnUnselectedError,
@@ -98,27 +88,19 @@ export {
98
88
  /** The HTTP projection: `GET /_x/query/<kebab>`, the URL `client()` derives. */
99
89
  export { toQueryRoute } from './http';
100
90
  export type { LiveCursor, LiveQuery, ResumeMode, ResumePlan, ToLiveOptions } from './live';
101
- export { planResume, seekOf, toLiveQuery } from './live';
91
+ export { planResume, seekOf, spendQueryLimit, toLiveQuery } from './live';
102
92
  export type { ChangeEvent, ChangeOp, Patch } from './matcher';
103
93
  export { assertMatchable, match, positionFor } from './matcher';
104
- export type { QueryToolAnswer, QueryToolDescriptor, QueryToolReadOptions } from './mcp-tool';
105
- export { isExposed, toQueryTool, toQueryTools } from './mcp-tool';
106
- /**
107
- * Path derivation only. There is no `toToolName`: an MCP tool is served under the export name
108
- * verbatim, and an exported derivation would be a second way to spell one tool.
109
- */
110
- export { derivePath } from './naming';
111
94
  /**
112
95
  * The read half of `openapi.json`. `@ultimat3/cli` merges these paths into `@ultimat3/action`'s
113
96
  * `buildOpenApi` document — the two packages are one tier and cannot compose each other.
114
97
  */
115
98
  export { queryOpenApiPaths, toQueryOpenApiOperation } from './openapi';
116
99
  /**
117
- * The two search-string keys that page `GET /_x/query/<name>` — `_first` and `_after` — and the
118
- * shape the typed client's `.page()` takes. `MAX_PAGE_SIZE` is the bound both ends check.
100
+ * The shape the typed client's `.page()` takes. The bound both ends check is `@ultimat3/entity`'s
101
+ * `MAX_PAGE_SIZE` — imported from there, never re-published here.
119
102
  */
120
103
  export type { PageControls } from './page-controls';
121
- export { MAX_PAGE_SIZE } from './page-controls';
122
104
  /**
123
105
  * The shapes `query.page(input, { first, after })` takes and answers with. `paginate` itself is
124
106
  * deliberately unexported: a page is the read's own answer, and a second, importable way to ask
@@ -128,19 +110,12 @@ export { MAX_PAGE_SIZE } from './page-controls';
128
110
  export type { Page, PaginateArgs } from './pagination';
129
111
  export type { QueryPolicy, QuerySubject, QuerySurface } from './policy-gate';
130
112
  /**
131
- * `policyCapability` is the display label; `policyPermissions` is what a report MATCHES on.
132
- * `admitsAnonymous` is `@ultimat3/policy`'s, re-exported here beside them: it is what
133
- * `toQueryRoute` derives `meta.auth` from, so a plain `route` sets that field from the same walk
134
- * rather than re-reading the root combinator.
113
+ * The one authz gate for reads, named after what it guards. The display label
114
+ * (`policyCapability`), what a report MATCHES on (`policyPermissions`) and what `toQueryRoute`
115
+ * derives `meta.auth` from (`admitsAnonymous`) are `@ultimat3/policy`'s, and the anonymous →
116
+ * `null` mapping (`actorOf`) is `@ultimat3/core`'s — imported from there, never re-published.
135
117
  */
136
- export {
137
- actorOf,
138
- admitsAnonymous,
139
- guard,
140
- guardBeforeInput,
141
- policyCapability,
142
- policyPermissions,
143
- } from './policy-gate';
118
+ export { guardQuery, guardQueryBeforeInput } from './policy-gate';
144
119
  export type {
145
120
  AnyQuery,
146
121
  Query,
@@ -148,10 +123,6 @@ export type {
148
123
  QueryDef,
149
124
  QueryDescriptor,
150
125
  QueryFacade,
151
- QueryListFilterOp,
152
- QueryListParams,
153
- QueryMcp,
154
- QueryMcpAnnotations,
155
126
  QueryOptions,
156
127
  QueryRateLimit,
157
128
  SourceOptions,
@@ -165,7 +136,7 @@ export {
165
136
  listQueries,
166
137
  registerQueries,
167
138
  registerQuery,
168
- resetRegistry,
139
+ resetQueries,
169
140
  } from './registry';
170
141
  /** The query FACTORY over an entity's searchable columns — a `query`, never a ninth primitive. */
171
142
  export type { SearchChain, SearchDef, SearchInput, SearchPage } from './search';
@@ -180,8 +151,9 @@ export {
180
151
  } from './shape';
181
152
  /** What a `single: true` read answers — one rule, also `@ultimat3/mcp`'s served tool's. */
182
153
  export { readAnswer } from './single-answer';
183
- export type { RowProvider, SqlSource, SqlText } from './source';
154
+ /** `Builder` is a type only: `from()` is the one way to build one, so there is no `new Builder()`. */
155
+ export type { Builder, RowProvider, SqlSource, SqlText } from './source';
184
156
  /** `isAfterKey` is the one definition of "after this position" — both seek paths use it. */
185
- export { Builder, from, isAfterKey } from './source';
157
+ export { from, isAfterKey } from './source';
186
158
  export type { ExplainResult, QuerySqlInfo } from './sql';
187
- export { describeSql, explain } from './sql';
159
+ export { describeSql, explainQuery } from './sql';
@@ -20,7 +20,7 @@ import { QueryInputUnencodableError } from './errors';
20
20
  import { PAGE_CONTROL_KEYS } from './page-controls';
21
21
 
22
22
  /** Node kinds whose value is a structure, not characters. `money` is `{ minor, currency }`. */
23
- const STRUCTURAL = new Set(['object', 'record', 'money']);
23
+ const STRUCTURAL = new Set(['object', 'record', 'money', 'json']);
24
24
 
25
25
  /**
26
26
  * The first key this input could not put on a wire, or `undefined`. One key, not a list: a fix