@ultimat3/query 7.0.0 → 9.0.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
@@ -145,6 +145,20 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
145
145
  authz stage deciding first would be a second authz system holding raw strings — and would
146
146
  demand an `authorize` hook to decide at all. `http.test.ts` drives both over the real pipeline
147
147
  with no hook wired and counts the evaluations: exactly one.
148
+ - **`meta.auth` is derived from a WALK of the policy tree**, never from the root combinator.
149
+ `target.policy.kind === 'allow'` answered `'required'` for `or(allow(), can('x:y'))`, so the
150
+ pipeline's `auth` stage 401'd an anonymous caller the policy itself ALLOWS — while the MCP tool
151
+ and a direct server read let that caller through the same object. One policy, a different answer
152
+ per surface. `'public'` here is `meta.auth` only: the read is still `cache: no-store` and
153
+ `runQuery` still evaluates the policy per caller.
154
+ **`admitsAnonymous` is `@ultimat3/policy`'s** (`policy.ts`, beside `policyPermissions`) and
155
+ reaches this package through `policy-gate.ts` like every other authz question — never a copy
156
+ here. It cannot be one: `@ultimat3/action` needs the identical answer and is the same tier, so a
157
+ copy in either is a second answer for the other. It is EXACT rather than heuristic — with
158
+ `actor === null`, `can()` short-circuits before its predicate and `allow()`/`deny()` ignore their
159
+ arguments, so the tree alone decides. `packages/policy/src/policy.test.ts` asserts it against
160
+ `policy.run({ actor: null })` itself, case for case; `http.test.ts` proves this projection reads
161
+ the answer, over the real pipeline.
148
162
  - `registry.ts` announces `registerQueries` in core's registrar table at import. That is how
149
163
  `defineApi({ queries })` in `@ultimat3/action` registers a read without importing this package
150
164
  sideways. Never remove the announcement: `defineApi` would then throw `X_REGISTRAR_MISSING`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/query",
3
- "version": "7.0.0",
3
+ "version": "9.0.0",
4
4
  "description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,10 +31,10 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/cache": "7.0.0",
35
- "@ultimat3/core": "7.0.0",
36
- "@ultimat3/http": "7.0.0",
37
- "@ultimat3/policy": "7.0.0",
38
- "@ultimat3/schema": "7.0.0"
34
+ "@ultimat3/cache": "9.0.0",
35
+ "@ultimat3/core": "9.0.0",
36
+ "@ultimat3/http": "9.0.0",
37
+ "@ultimat3/policy": "9.0.0",
38
+ "@ultimat3/schema": "9.0.0"
39
39
  }
40
40
  }
package/src/http.ts CHANGED
@@ -14,7 +14,7 @@ import type { Deprecation } from './deprecation';
14
14
  import { applyHeaders, recordDeprecatedCall, renderDeprecation } from './deprecation';
15
15
  import { QueryDeprecationInvalidError } from './errors';
16
16
  import { derivePath } from './naming';
17
- import { policyCapability } from './policy-gate';
17
+ import { admitsAnonymous, policyCapability } from './policy-gate';
18
18
  import type { AnyQuery } from './query';
19
19
  import { queryName, runQuery } from './read';
20
20
 
@@ -58,9 +58,12 @@ export function toQueryRoute(target: AnyQuery): Route {
58
58
 
59
59
  const meta: RouteMeta = {
60
60
  name,
61
- // `allow(...)` is the only way a read is public, and saying so explicitly is what
62
- // keeps "forgot the policy" from ever looking like "meant to be readable".
63
- auth: target.policy.kind === 'allow' ? 'public' : 'required',
61
+ // Derived from a WALK of the policy tree, never from the root combinator alone.
62
+ // `policy.kind === 'allow'` answered `required` for `or(allow(), can('x:y'))`, so the pipeline
63
+ // 401'd an anonymous caller the policy itself allows while the MCP tool and a direct server
64
+ // read let the same caller through the same object. `public` here is not "unguarded":
65
+ // `enforcedBy: 'handler'` below means `runQuery` still evaluates the policy for every read.
66
+ auth: admitsAnonymous(target.policy) ? 'public' : 'required',
64
67
  policy: policyCapability(target.policy),
65
68
  // `runQuery` is this route's one evaluation and it decides from the PARSED input the
66
69
  // rule reads (`ownsOrg(actor, input.orgId)`); the stage would decide the same policy
package/src/index.ts CHANGED
@@ -70,8 +70,19 @@ export { derivePath, toKebabCase } from './naming';
70
70
  */
71
71
  export type { Page, PaginateArgs } from './pagination';
72
72
  export type { QueryPolicy, QuerySubject, QuerySurface } from './policy-gate';
73
- /** `policyCapability` is the display label; `policyPermissions` is what a report MATCHES on. */
74
- export { actorOf, guard, policyCapability, policyPermissions } from './policy-gate';
73
+ /**
74
+ * `policyCapability` is the display label; `policyPermissions` is what a report MATCHES on.
75
+ * `admitsAnonymous` is `@ultimat3/policy`'s, re-exported here beside them: it is what
76
+ * `toQueryRoute` derives `meta.auth` from, so a plain `route` sets that field from the same walk
77
+ * rather than re-reading the root combinator.
78
+ */
79
+ export {
80
+ actorOf,
81
+ admitsAnonymous,
82
+ guard,
83
+ policyCapability,
84
+ policyPermissions,
85
+ } from './policy-gate';
75
86
  export type {
76
87
  AnyQuery,
77
88
  Query,
@@ -7,7 +7,11 @@
7
7
  import type { Actor, Ctx } from '@ultimat3/core';
8
8
  import { assertNever, isAnonymous } from '@ultimat3/core';
9
9
  import type { Policy, Surface as PolicySurface } from '@ultimat3/policy';
10
- import { enforce, policyPermissions as flattenedPermissions } from '@ultimat3/policy';
10
+ import {
11
+ enforce,
12
+ policyPermissions as flattenedPermissions,
13
+ admitsAnonymous as policyAdmitsAnonymous,
14
+ } from '@ultimat3/policy';
11
15
  import { QueryDeniedError } from './errors';
12
16
 
13
17
  /** Policies are opaque here: we evaluate them, we never introspect their rules. */
@@ -75,3 +79,16 @@ export function policyCapability(policy: QueryPolicy): string {
75
79
  export function policyPermissions(policy: QueryPolicy): readonly string[] {
76
80
  return flattenedPermissions(policy);
77
81
  }
82
+
83
+ /**
84
+ * Whether a policy admits an ANONYMOUS caller — `@ultimat3/policy`'s answer, re-exported here so
85
+ * `http.ts` reads it through this file like every other authz question. `toQueryRoute` derives
86
+ * `meta.auth` from it, never from `policy.kind === 'allow'`: that read looked at the ROOT
87
+ * combinator only, so `or(allow(), can('x:y'))` was 401'd by the pipeline before `runQuery` ran
88
+ * while the MCP tool and a direct server read allowed it. `true` is `meta.auth` only — the read is
89
+ * still `no-store` and `runQuery` still evaluates the policy per caller. Declared once in
90
+ * `policy.ts`, exactly as `policyPermissions` is: the answer is a property of the combinators.
91
+ */
92
+ export function admitsAnonymous(policy: QueryPolicy): boolean {
93
+ return policyAdmitsAnonymous(policy);
94
+ }