@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 +14 -0
- package/package.json +6 -6
- package/src/http.ts +7 -4
- package/src/index.ts +13 -2
- package/src/policy-gate.ts +18 -1
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": "
|
|
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": "
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/http": "
|
|
37
|
-
"@ultimat3/policy": "
|
|
38
|
-
"@ultimat3/schema": "
|
|
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
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
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
|
-
/**
|
|
74
|
-
|
|
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,
|
package/src/policy-gate.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|