cvx-kit 0.1.14 → 0.1.15

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/CHANGELOG.md CHANGED
@@ -12,6 +12,19 @@ are this file's matching section.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.1.15] - 2026-10-11
16
+
17
+ ### Added
18
+
19
+ - Declare a global query engine with only `createQueryEngine({ schema, relations })`.
20
+ - Load cursor secrets internally from server environment configuration on first pagination.
21
+ - Configure optional engine overrides in a separate second argument, while preserving existing declarations.
22
+
23
+ ### Changed
24
+
25
+ - Document global engine setup, server cursor secrets, and the purpose of advanced options.
26
+ - Publish releases after successful CI for the exact main commit, without repeating tests in the release workflow.
27
+
15
28
  ## [0.1.14] - 2026-10-11
16
29
 
17
30
  ### Added
package/README.md CHANGED
@@ -123,17 +123,15 @@ Missing and unreadable targets have the same public result.
123
123
  import { createQueryEngine } from 'cvx-kit/querying'
124
124
  import schema from './schema'
125
125
  import { relations } from './relations'
126
- import { cursor } from './functions'
127
126
 
128
- export const querying = createQueryEngine({
129
- schema,
130
- relations,
131
- cursor,
132
- })
127
+ export const querying = createQueryEngine({ schema, relations })
133
128
  ```
134
129
 
135
- `cursor.keys` is a server-only map of key IDs to 64-byte `Uint8Array` secrets.
136
- Keep key material out of arguments, client code, and logs. The default codec
130
+ One engine serves every registered table. Cursor keys load internally on first
131
+ pagination from server environment variables `CVX_QUERY_CURSOR_KEYS_JSON`
132
+ (key IDs mapped to 128-character hex secrets) and `CVX_QUERY_CURSOR_CURRENT_KEY_ID`.
133
+ Keep key material out of arguments, client code, and logs. Optional overrides
134
+ belong in a separate second argument; see [querying](src/docs/querying.md). The default codec
137
135
  uses authenticated AES-SIV encryption without a plaintext fallback; identical
138
136
  payloads under the same binding reveal equality. Tokens bind filters, selected
139
137
  output, index, order, relation ancestry, principal/tenant/security scope, and
@@ -31,6 +31,11 @@ export declare class CursorConfigurationError extends Error {
31
31
  readonly _tag = "CursorConfigurationError";
32
32
  constructor();
33
33
  }
34
+ /** Private lazy server configuration. Call only at the first pagination boundary. */
35
+ export declare function createEnvironmentCursorCodec(): Readonly<{
36
+ seal(raw: string, binding: CursorBinding): string;
37
+ open(token: string, binding: CursorBinding): string;
38
+ }>;
34
39
  /**
35
40
  * Deterministic RFC 5297 AES-SIV: equal plaintext and binding leak equality.
36
41
  * One navigation-boundary family permits native split/continue tokens in start/end
@@ -1,2 +1,2 @@
1
- import{canonicalConvexBytes as e}from"../modules/contracts/idempotency.mjs";import{aessiv as t}from"@noble/ciphers/aes.js";import{bytesToHex as n,hexToBytes as r}from"@noble/ciphers/utils.js";var i=class extends Error{_tag=`CursorError`;code=`invalid-cursor`;constructor(){super(`Invalid cursor`),this.name=`CursorError`}},a=class extends Error{_tag=`CursorConfigurationError`;constructor(){super(`Invalid cursor server configuration`),this.name=`CursorConfigurationError`}};const o=Object.freeze({maxDepth:24,maxNodes:2048,maxBytes:16384,maxArrayLength:512,maxObjectFields:128}),s=8192,c=new TextEncoder,l=new TextDecoder(`utf-8`,{fatal:!0}),u=/^[A-Za-z0-9_-]{1,32}$/;function d(e){let o,d,f=/* @__PURE__ */ new Map;try{if(o=e.version,d=e.currentKeyId,typeof o!=`string`||typeof d!=`string`||!u.test(o)||!u.test(d))throw new a;let t=Object.entries(e.keys);if(!t.length)throw new a;for(let[e,n]of t){if(!u.test(e)||!(n instanceof Uint8Array)||n.byteLength!==64)throw new a;f.set(e,Uint8Array.from(n))}if(!f.has(d))throw new a}catch{throw new a}return Object.freeze({seal(e,r){try{if(typeof e!=`string`||e.length>s||c.encode(e).length>s)throw new i;let a=c.encode(JSON.stringify(e)),l=t(f.get(d),p(o,d,r));return`pc.${o}.${d}.${n(l.encrypt(a))}`}catch{throw new i}},open(e,n){try{if(typeof e!=`string`||e.length>98410)throw new i;let a=e.split(`.`);if(a.length!==4||a[0]!==`pc`||a[1]!==o||!u.test(a[2]))throw new i;let d=a[2],m=a[3],h=f.get(d);if(!h||m.length<36||m.length>98340||m.length%2||!/^[0-9a-f]+$/.test(m))throw new i;let g=t(h,p(o,d,n)).decrypt(r(m));if(g.length>49154)throw new i;let _=l.decode(g),v=JSON.parse(_);if(typeof v!=`string`||v.length>s||c.encode(v).length>s||JSON.stringify(v)!==_)throw new i;return v}catch{throw new i}}})}function f(e){let t=/* @__PURE__ */ new Set,n=0;function r(e,a){if(++n>o.maxNodes||a>o.maxDepth)throw new i;if(typeof e!=`object`||!e||e instanceof ArrayBuffer)return;if(t.has(e))throw new i;let s=Array.isArray(e);if(s&&Object.getPrototypeOf(e)!==Array.prototype||!s&&Object.getPrototypeOf(e)!==Object.prototype&&Object.getPrototypeOf(e)!==null||Object.getOwnPropertySymbols(e).length)throw new i;let c=Object.getOwnPropertyNames(e);if(c.length>(s?o.maxArrayLength+1:o.maxObjectFields))throw new i;t.add(e);try{for(let t of c){let n=Object.getOwnPropertyDescriptor(e,t);if(!(`value`in n))throw new i;if(s&&t===`length`){if(n.value>o.maxArrayLength)throw new i;continue}if(!n.enumerable||s&&(!/^(0|[1-9][0-9]*)$/.test(t)||Number(t)>=e.length))throw new i;r(n.value,a+1)}}finally{t.delete(e)}}r(e,0)}function p(t,n,r){if(f(r),r.clientFilters===void 0||r.serverFilters===void 0)throw new i;let{table:a,order:s,principal:l,policyVersion:u,engineVersion:d,registryVersion:p}=r;for(let e of[a,l,u,d,p])if(typeof e!=`string`||!e.length)throw new i;if(s!==`asc`&&s!==`desc`)throw new i;for(let e of[r.index,r.parent,r.tenant])if(e!=null&&typeof e!=`string`)throw new i;if(!Array.isArray(r.relationPath)||r.relationPath.some(e=>typeof e!=`string`))throw new i;return c.encode(e({domain:`cvx-query-cursor`,version:t,keyId:n,table:a,index:r.index??null,order:s,clientFilters:r.clientFilters,serverFilters:r.serverFilters,relationPath:r.relationPath,parent:r.parent??null,principal:l,tenant:r.tenant??null,policyVersion:u,engineVersion:d,registryVersion:p},o))}export{a as CursorConfigurationError,i as CursorError,d as createCursorCodec};
1
+ import{canonicalConvexBytes as e}from"../modules/contracts/idempotency.mjs";import{aessiv as t}from"@noble/ciphers/aes.js";import{bytesToHex as n,hexToBytes as r}from"@noble/ciphers/utils.js";var i=class extends Error{_tag=`CursorError`;code=`invalid-cursor`;constructor(){super(`Invalid cursor`),this.name=`CursorError`}},a=class extends Error{_tag=`CursorConfigurationError`;constructor(){super(`Invalid cursor server configuration`),this.name=`CursorConfigurationError`}};const o=Object.freeze({maxDepth:24,maxNodes:2048,maxBytes:16384,maxArrayLength:512,maxObjectFields:128}),s=8192,c=new TextEncoder,l=new TextDecoder(`utf-8`,{fatal:!0}),u=/^[A-Za-z0-9_-]{1,32}$/;function d(){try{let e=process.env.CVX_QUERY_CURSOR_KEYS_JSON,t=process.env.CVX_QUERY_CURSOR_CURRENT_KEY_ID;if(!e||!t)throw new a;let n=JSON.parse(e);if(typeof n!=`object`||!n||Array.isArray(n))throw new a;return f({version:`1`,currentKeyId:t,keys:Object.fromEntries(Object.entries(n).map(([e,t])=>{if(typeof t!=`string`||!/^[0-9a-fA-F]{128}$/.test(t))throw new a;return[e,r(t)]}))})}catch{throw new a}}function f(e){let o,d,f=/* @__PURE__ */ new Map;try{if(o=e.version,d=e.currentKeyId,typeof o!=`string`||typeof d!=`string`||!u.test(o)||!u.test(d))throw new a;let t=Object.entries(e.keys);if(!t.length)throw new a;for(let[e,n]of t){if(!u.test(e)||!(n instanceof Uint8Array)||n.byteLength!==64)throw new a;f.set(e,Uint8Array.from(n))}if(!f.has(d))throw new a}catch{throw new a}return Object.freeze({seal(e,r){try{if(typeof e!=`string`||e.length>s||c.encode(e).length>s)throw new i;let a=c.encode(JSON.stringify(e)),l=t(f.get(d),m(o,d,r));return`pc.${o}.${d}.${n(l.encrypt(a))}`}catch{throw new i}},open(e,n){try{if(typeof e!=`string`||e.length>98410)throw new i;let a=e.split(`.`);if(a.length!==4||a[0]!==`pc`||a[1]!==o||!u.test(a[2]))throw new i;let d=a[2],p=a[3],h=f.get(d);if(!h||p.length<36||p.length>98340||p.length%2||!/^[0-9a-f]+$/.test(p))throw new i;let g=t(h,m(o,d,n)).decrypt(r(p));if(g.length>49154)throw new i;let _=l.decode(g),v=JSON.parse(_);if(typeof v!=`string`||v.length>s||c.encode(v).length>s||JSON.stringify(v)!==_)throw new i;return v}catch{throw new i}}})}function p(e){let t=/* @__PURE__ */ new Set,n=0;function r(e,a){if(++n>o.maxNodes||a>o.maxDepth)throw new i;if(typeof e!=`object`||!e||e instanceof ArrayBuffer)return;if(t.has(e))throw new i;let s=Array.isArray(e);if(s&&Object.getPrototypeOf(e)!==Array.prototype||!s&&Object.getPrototypeOf(e)!==Object.prototype&&Object.getPrototypeOf(e)!==null||Object.getOwnPropertySymbols(e).length)throw new i;let c=Object.getOwnPropertyNames(e);if(c.length>(s?o.maxArrayLength+1:o.maxObjectFields))throw new i;t.add(e);try{for(let t of c){let n=Object.getOwnPropertyDescriptor(e,t);if(!(`value`in n))throw new i;if(s&&t===`length`){if(n.value>o.maxArrayLength)throw new i;continue}if(!n.enumerable||s&&(!/^(0|[1-9][0-9]*)$/.test(t)||Number(t)>=e.length))throw new i;r(n.value,a+1)}}finally{t.delete(e)}}r(e,0)}function m(t,n,r){if(p(r),r.clientFilters===void 0||r.serverFilters===void 0)throw new i;let{table:a,order:s,principal:l,policyVersion:u,engineVersion:d,registryVersion:f}=r;for(let e of[a,l,u,d,f])if(typeof e!=`string`||!e.length)throw new i;if(s!==`asc`&&s!==`desc`)throw new i;for(let e of[r.index,r.parent,r.tenant])if(e!=null&&typeof e!=`string`)throw new i;if(!Array.isArray(r.relationPath)||r.relationPath.some(e=>typeof e!=`string`))throw new i;return c.encode(e({domain:`cvx-query-cursor`,version:t,keyId:n,table:a,index:r.index??null,order:s,clientFilters:r.clientFilters,serverFilters:r.serverFilters,relationPath:r.relationPath,parent:r.parent??null,principal:l,tenant:r.tenant??null,policyVersion:u,engineVersion:d,registryVersion:f},o))}export{a as CursorConfigurationError,i as CursorError,f as createCursorCodec,d as createEnvironmentCursorCodec};
2
2
  //# sourceMappingURL=cursors.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"cursors.mjs","names":["CursorError","CursorConfigurationError","bounds","maxPlaintextBytes","encoder","decoder","identifier","createCursorCodec","version","currentKeyId","keys","config","entries","id","key","raw","payload","cipher","aessiv","aad","binding","bytesToHex","token","parts","keyId","encoded","plaintext","hexToBytes","json","checkDescriptors","ancestors","nodes","visit","depth","current","array","names","name","descriptor","value","field","table","principal","policyVersion","engineVersion","registryVersion","order","part","canonicalConvexBytes"],"sources":["../../src/querying/cursors.ts"],"sourcesContent":["/* oxlint-disable anti-slop/no-runtime-typeof, anti-slop/no-unknown-parameters -- Cursor transport and canonical values are explicit validation boundaries. */\nimport { aessiv } from '@noble/ciphers/aes.js'\nimport { bytesToHex, hexToBytes } from '@noble/ciphers/utils.js'\nimport type { Value } from 'convex/values'\nimport { canonicalConvexBytes } from '../modules/contracts/idempotency'\n\n/** Private engine contract. Bindings must come from trusted normalized engine state, never query args. */\nexport interface CursorBinding {\n\treadonly table: string\n\treadonly index?: string | null\n\treadonly order: 'asc' | 'desc'\n\treadonly clientFilters: Value\n\treadonly serverFilters: Value\n\treadonly relationPath: readonly string[]\n\treadonly parent?: string | null\n\treadonly principal: string\n\treadonly tenant?: string | null\n\treadonly policyVersion: string\n\treadonly engineVersion: string\n\treadonly registryVersion: string\n\t/** Deliberately excluded from authenticated identity. */\n\treadonly pageSize?: number\n}\nexport interface CursorCodecConfig {\n\treadonly version: string\n\treadonly currentKeyId: string\n\treadonly keys: Readonly<Record<string, Uint8Array>>\n}\nexport class CursorError extends Error {\n\treadonly _tag = 'CursorError'\n\treadonly code = 'invalid-cursor'\n\tconstructor() {\n\t\tsuper('Invalid cursor')\n\t\tthis.name = 'CursorError'\n\t}\n}\nexport class CursorConfigurationError extends Error {\n\treadonly _tag = 'CursorConfigurationError'\n\tconstructor() {\n\t\tsuper('Invalid cursor server configuration')\n\t\tthis.name = 'CursorConfigurationError'\n\t}\n}\n\nconst bounds = Object.freeze({\n\tmaxDepth: 24,\n\tmaxNodes: 2048,\n\tmaxBytes: 16384,\n\tmaxArrayLength: 512,\n\tmaxObjectFields: 128,\n})\nconst maxPlaintextBytes = 8192\n// JSON string encoding preserves even lone UTF-16 surrogates without lossy UTF-8 replacement.\nconst maxPayloadBytes = maxPlaintextBytes * 6 + 2\nconst maxCiphertextBytes = maxPayloadBytes + 16\nconst maxTokenLength = maxCiphertextBytes * 2 + 70\nconst encoder = new TextEncoder()\nconst decoder = new TextDecoder('utf-8', { fatal: true })\nconst identifier = /^[A-Za-z0-9_-]{1,32}$/\n\n/**\n * Deterministic RFC 5297 AES-SIV: equal plaintext and binding leak equality.\n * One navigation-boundary family permits native split/continue tokens in start/end\n * slots. Relation identity remains in the binding. No nonce/randomness or plaintext fallback. Opening is not authorization: the engine\n * must recheck current policy on every future request, including nested continuations.\n */\nexport function createCursorCodec(config: CursorCodecConfig) {\n\tlet version: string, currentKeyId: string\n\tconst keys = new Map<string, Uint8Array>()\n\ttry {\n\t\tversion = config.version\n\t\tcurrentKeyId = config.currentKeyId\n\t\tif (\n\t\t\ttypeof version !== 'string' ||\n\t\t\ttypeof currentKeyId !== 'string' ||\n\t\t\t!identifier.test(version) ||\n\t\t\t!identifier.test(currentKeyId)\n\t\t)\n\t\t\tthrow new CursorConfigurationError()\n\t\tconst entries = Object.entries(config.keys)\n\t\tif (!entries.length) throw new CursorConfigurationError()\n\t\tfor (const [id, key] of entries) {\n\t\t\tif (!identifier.test(id) || !(key instanceof Uint8Array) || key.byteLength !== 64)\n\t\t\t\tthrow new CursorConfigurationError()\n\t\t\tkeys.set(id, Uint8Array.from(key))\n\t\t}\n\t\tif (!keys.has(currentKeyId)) throw new CursorConfigurationError()\n\t} catch {\n\t\tthrow new CursorConfigurationError()\n\t}\n\treturn Object.freeze({\n\t\tseal(raw: string, binding: CursorBinding): string {\n\t\t\ttry {\n\t\t\t\tif (\n\t\t\t\t\ttypeof raw !== 'string' ||\n\t\t\t\t\traw.length > maxPlaintextBytes ||\n\t\t\t\t\tencoder.encode(raw).length > maxPlaintextBytes\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\tconst payload = encoder.encode(JSON.stringify(raw))\n\t\t\t\tconst cipher = aessiv(keys.get(currentKeyId)!, aad(version, currentKeyId, binding))\n\t\t\t\treturn `pc.${version}.${currentKeyId}.${bytesToHex(cipher.encrypt(payload))}`\n\t\t\t} catch {\n\t\t\t\tthrow new CursorError()\n\t\t\t}\n\t\t},\n\t\topen(token: string, binding: CursorBinding): string {\n\t\t\ttry {\n\t\t\t\tif (typeof token !== 'string' || token.length > maxTokenLength) throw new CursorError()\n\t\t\t\tconst parts = token.split('.')\n\t\t\t\tif (\n\t\t\t\t\tparts.length !== 4 ||\n\t\t\t\t\tparts[0] !== 'pc' ||\n\t\t\t\t\tparts[1] !== version ||\n\t\t\t\t\t!identifier.test(parts[2]!)\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\tconst keyId = parts[2]!,\n\t\t\t\t\tencoded = parts[3]!,\n\t\t\t\t\tkey = keys.get(keyId)\n\t\t\t\tif (\n\t\t\t\t\t!key ||\n\t\t\t\t\tencoded.length < 36 ||\n\t\t\t\t\tencoded.length > maxCiphertextBytes * 2 ||\n\t\t\t\t\tencoded.length % 2 ||\n\t\t\t\t\t!/^[0-9a-f]+$/.test(encoded)\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\tconst cipher = aessiv(key, aad(version, keyId, binding))\n\t\t\t\tconst plaintext = cipher.decrypt(hexToBytes(encoded))\n\t\t\t\tif (plaintext.length > maxPayloadBytes) throw new CursorError()\n\t\t\t\tconst json = decoder.decode(plaintext)\n\t\t\t\tconst raw: unknown = JSON.parse(json)\n\t\t\t\tif (\n\t\t\t\t\ttypeof raw !== 'string' ||\n\t\t\t\t\traw.length > maxPlaintextBytes ||\n\t\t\t\t\tencoder.encode(raw).length > maxPlaintextBytes ||\n\t\t\t\t\tJSON.stringify(raw) !== json\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\treturn raw\n\t\t\t} catch {\n\t\t\t\tthrow new CursorError()\n\t\t\t}\n\t\t},\n\t})\n}\n\n/** Reject accessor-bearing arrays/dictionaries before the shared canonical encoder reads values. */\nfunction checkDescriptors(value: unknown): void {\n\tconst ancestors = new Set<object>()\n\tlet nodes = 0\n\tfunction visit(current: unknown, depth: number): void {\n\t\tif (++nodes > bounds.maxNodes || depth > bounds.maxDepth) throw new CursorError()\n\t\tif (current === null || typeof current !== 'object' || current instanceof ArrayBuffer) return\n\t\tif (ancestors.has(current)) throw new CursorError()\n\t\tconst array = Array.isArray(current)\n\t\tif (array && Object.getPrototypeOf(current) !== Array.prototype) throw new CursorError()\n\t\tif (\n\t\t\t!array &&\n\t\t\tObject.getPrototypeOf(current) !== Object.prototype &&\n\t\t\tObject.getPrototypeOf(current) !== null\n\t\t)\n\t\t\tthrow new CursorError()\n\t\tif (Object.getOwnPropertySymbols(current).length) throw new CursorError()\n\t\tconst names = Object.getOwnPropertyNames(current)\n\t\tif (names.length > (array ? bounds.maxArrayLength + 1 : bounds.maxObjectFields))\n\t\t\tthrow new CursorError()\n\t\tancestors.add(current)\n\t\ttry {\n\t\t\tfor (const name of names) {\n\t\t\t\tconst descriptor = Object.getOwnPropertyDescriptor(current, name)!\n\t\t\t\tif (!('value' in descriptor)) throw new CursorError()\n\t\t\t\tif (array && name === 'length') {\n\t\t\t\t\tif (descriptor.value > bounds.maxArrayLength) throw new CursorError()\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\t!descriptor.enumerable ||\n\t\t\t\t\t(array && (!/^(0|[1-9][0-9]*)$/.test(name) || Number(name) >= current.length))\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\tvisit(descriptor.value, depth + 1)\n\t\t\t}\n\t\t} finally {\n\t\t\tancestors.delete(current)\n\t\t}\n\t}\n\tvisit(value, 0)\n}\nfunction aad(version: string, keyId: string, binding: CursorBinding): Uint8Array {\n\tcheckDescriptors(binding)\n\tif (binding.clientFilters === undefined || binding.serverFilters === undefined)\n\t\tthrow new CursorError()\n\tconst { table, order, principal, policyVersion, engineVersion, registryVersion } = binding\n\tfor (const field of [table, principal, policyVersion, engineVersion, registryVersion])\n\t\tif (typeof field !== 'string' || !field.length) throw new CursorError()\n\tif (order !== 'asc' && order !== 'desc') throw new CursorError()\n\tfor (const field of [binding.index, binding.parent, binding.tenant])\n\t\tif (field != null && typeof field !== 'string') throw new CursorError()\n\tif (\n\t\t!Array.isArray(binding.relationPath) ||\n\t\tbinding.relationPath.some((part) => typeof part !== 'string')\n\t)\n\t\tthrow new CursorError()\n\treturn encoder.encode(\n\t\tcanonicalConvexBytes(\n\t\t\t{\n\t\t\t\tdomain: 'cvx-query-cursor',\n\t\t\t\tversion,\n\t\t\t\tkeyId,\n\t\t\t\ttable,\n\t\t\t\tindex: binding.index ?? null,\n\t\t\t\torder,\n\t\t\t\tclientFilters: binding.clientFilters,\n\t\t\t\tserverFilters: binding.serverFilters,\n\t\t\t\trelationPath: binding.relationPath,\n\t\t\t\tparent: binding.parent ?? null,\n\t\t\t\tprincipal,\n\t\t\t\ttenant: binding.tenant ?? null,\n\t\t\t\tpolicyVersion,\n\t\t\t\tengineVersion,\n\t\t\t\tregistryVersion,\n\t\t\t},\n\t\t\tbounds,\n\t\t),\n\t)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cursors.mjs","names":["CursorError","CursorConfigurationError","bounds","maxPlaintextBytes","encoder","decoder","identifier","createEnvironmentCursorCodec","encoded","currentKeyId","parsed","createCursorCodec","id","key","hexToBytes","version","keys","config","entries","raw","payload","cipher","aessiv","aad","binding","bytesToHex","token","parts","keyId","plaintext","json","checkDescriptors","ancestors","nodes","visit","depth","current","array","names","name","descriptor","value","field","table","principal","policyVersion","engineVersion","registryVersion","order","part","canonicalConvexBytes"],"sources":["../../src/querying/cursors.ts"],"sourcesContent":["/* oxlint-disable anti-slop/no-runtime-typeof, anti-slop/no-unknown-parameters -- Cursor transport and canonical values are explicit validation boundaries. */\nimport { aessiv } from '@noble/ciphers/aes.js'\nimport { bytesToHex, hexToBytes } from '@noble/ciphers/utils.js'\nimport type { Value } from 'convex/values'\nimport { canonicalConvexBytes } from '../modules/contracts/idempotency'\n\n/** Private engine contract. Bindings must come from trusted normalized engine state, never query args. */\nexport interface CursorBinding {\n\treadonly table: string\n\treadonly index?: string | null\n\treadonly order: 'asc' | 'desc'\n\treadonly clientFilters: Value\n\treadonly serverFilters: Value\n\treadonly relationPath: readonly string[]\n\treadonly parent?: string | null\n\treadonly principal: string\n\treadonly tenant?: string | null\n\treadonly policyVersion: string\n\treadonly engineVersion: string\n\treadonly registryVersion: string\n\t/** Deliberately excluded from authenticated identity. */\n\treadonly pageSize?: number\n}\nexport interface CursorCodecConfig {\n\treadonly version: string\n\treadonly currentKeyId: string\n\treadonly keys: Readonly<Record<string, Uint8Array>>\n}\nexport class CursorError extends Error {\n\treadonly _tag = 'CursorError'\n\treadonly code = 'invalid-cursor'\n\tconstructor() {\n\t\tsuper('Invalid cursor')\n\t\tthis.name = 'CursorError'\n\t}\n}\nexport class CursorConfigurationError extends Error {\n\treadonly _tag = 'CursorConfigurationError'\n\tconstructor() {\n\t\tsuper('Invalid cursor server configuration')\n\t\tthis.name = 'CursorConfigurationError'\n\t}\n}\n\nconst bounds = Object.freeze({\n\tmaxDepth: 24,\n\tmaxNodes: 2048,\n\tmaxBytes: 16384,\n\tmaxArrayLength: 512,\n\tmaxObjectFields: 128,\n})\nconst maxPlaintextBytes = 8192\n// JSON string encoding preserves even lone UTF-16 surrogates without lossy UTF-8 replacement.\nconst maxPayloadBytes = maxPlaintextBytes * 6 + 2\nconst maxCiphertextBytes = maxPayloadBytes + 16\nconst maxTokenLength = maxCiphertextBytes * 2 + 70\nconst encoder = new TextEncoder()\nconst decoder = new TextDecoder('utf-8', { fatal: true })\nconst identifier = /^[A-Za-z0-9_-]{1,32}$/\n\n/** Private lazy server configuration. Call only at the first pagination boundary. */\nexport function createEnvironmentCursorCodec() {\n\ttry {\n\t\tconst encoded = process.env.CVX_QUERY_CURSOR_KEYS_JSON\n\t\tconst currentKeyId = process.env.CVX_QUERY_CURSOR_CURRENT_KEY_ID\n\t\tif (!encoded || !currentKeyId) throw new CursorConfigurationError()\n\t\tconst parsed: unknown = JSON.parse(encoded)\n\t\tif (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))\n\t\t\tthrow new CursorConfigurationError()\n\t\tconst keys = Object.fromEntries(\n\t\t\tObject.entries(parsed).map(([id, key]) => {\n\t\t\t\tif (typeof key !== 'string' || !/^[0-9a-fA-F]{128}$/.test(key))\n\t\t\t\t\tthrow new CursorConfigurationError()\n\t\t\t\treturn [id, hexToBytes(key)]\n\t\t\t}),\n\t\t)\n\t\treturn createCursorCodec({ version: '1', currentKeyId, keys })\n\t} catch {\n\t\t// Parsing/runtime failures must never expose key material or environment values.\n\t\tthrow new CursorConfigurationError()\n\t}\n}\n\n/**\n * Deterministic RFC 5297 AES-SIV: equal plaintext and binding leak equality.\n * One navigation-boundary family permits native split/continue tokens in start/end\n * slots. Relation identity remains in the binding. No nonce/randomness or plaintext fallback. Opening is not authorization: the engine\n * must recheck current policy on every future request, including nested continuations.\n */\nexport function createCursorCodec(config: CursorCodecConfig) {\n\tlet version: string, currentKeyId: string\n\tconst keys = new Map<string, Uint8Array>()\n\ttry {\n\t\tversion = config.version\n\t\tcurrentKeyId = config.currentKeyId\n\t\tif (\n\t\t\ttypeof version !== 'string' ||\n\t\t\ttypeof currentKeyId !== 'string' ||\n\t\t\t!identifier.test(version) ||\n\t\t\t!identifier.test(currentKeyId)\n\t\t)\n\t\t\tthrow new CursorConfigurationError()\n\t\tconst entries = Object.entries(config.keys)\n\t\tif (!entries.length) throw new CursorConfigurationError()\n\t\tfor (const [id, key] of entries) {\n\t\t\tif (!identifier.test(id) || !(key instanceof Uint8Array) || key.byteLength !== 64)\n\t\t\t\tthrow new CursorConfigurationError()\n\t\t\tkeys.set(id, Uint8Array.from(key))\n\t\t}\n\t\tif (!keys.has(currentKeyId)) throw new CursorConfigurationError()\n\t} catch {\n\t\tthrow new CursorConfigurationError()\n\t}\n\treturn Object.freeze({\n\t\tseal(raw: string, binding: CursorBinding): string {\n\t\t\ttry {\n\t\t\t\tif (\n\t\t\t\t\ttypeof raw !== 'string' ||\n\t\t\t\t\traw.length > maxPlaintextBytes ||\n\t\t\t\t\tencoder.encode(raw).length > maxPlaintextBytes\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\tconst payload = encoder.encode(JSON.stringify(raw))\n\t\t\t\tconst cipher = aessiv(keys.get(currentKeyId)!, aad(version, currentKeyId, binding))\n\t\t\t\treturn `pc.${version}.${currentKeyId}.${bytesToHex(cipher.encrypt(payload))}`\n\t\t\t} catch {\n\t\t\t\tthrow new CursorError()\n\t\t\t}\n\t\t},\n\t\topen(token: string, binding: CursorBinding): string {\n\t\t\ttry {\n\t\t\t\tif (typeof token !== 'string' || token.length > maxTokenLength) throw new CursorError()\n\t\t\t\tconst parts = token.split('.')\n\t\t\t\tif (\n\t\t\t\t\tparts.length !== 4 ||\n\t\t\t\t\tparts[0] !== 'pc' ||\n\t\t\t\t\tparts[1] !== version ||\n\t\t\t\t\t!identifier.test(parts[2]!)\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\tconst keyId = parts[2]!,\n\t\t\t\t\tencoded = parts[3]!,\n\t\t\t\t\tkey = keys.get(keyId)\n\t\t\t\tif (\n\t\t\t\t\t!key ||\n\t\t\t\t\tencoded.length < 36 ||\n\t\t\t\t\tencoded.length > maxCiphertextBytes * 2 ||\n\t\t\t\t\tencoded.length % 2 ||\n\t\t\t\t\t!/^[0-9a-f]+$/.test(encoded)\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\tconst cipher = aessiv(key, aad(version, keyId, binding))\n\t\t\t\tconst plaintext = cipher.decrypt(hexToBytes(encoded))\n\t\t\t\tif (plaintext.length > maxPayloadBytes) throw new CursorError()\n\t\t\t\tconst json = decoder.decode(plaintext)\n\t\t\t\tconst raw: unknown = JSON.parse(json)\n\t\t\t\tif (\n\t\t\t\t\ttypeof raw !== 'string' ||\n\t\t\t\t\traw.length > maxPlaintextBytes ||\n\t\t\t\t\tencoder.encode(raw).length > maxPlaintextBytes ||\n\t\t\t\t\tJSON.stringify(raw) !== json\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\treturn raw\n\t\t\t} catch {\n\t\t\t\tthrow new CursorError()\n\t\t\t}\n\t\t},\n\t})\n}\n\n/** Reject accessor-bearing arrays/dictionaries before the shared canonical encoder reads values. */\nfunction checkDescriptors(value: unknown): void {\n\tconst ancestors = new Set<object>()\n\tlet nodes = 0\n\tfunction visit(current: unknown, depth: number): void {\n\t\tif (++nodes > bounds.maxNodes || depth > bounds.maxDepth) throw new CursorError()\n\t\tif (current === null || typeof current !== 'object' || current instanceof ArrayBuffer) return\n\t\tif (ancestors.has(current)) throw new CursorError()\n\t\tconst array = Array.isArray(current)\n\t\tif (array && Object.getPrototypeOf(current) !== Array.prototype) throw new CursorError()\n\t\tif (\n\t\t\t!array &&\n\t\t\tObject.getPrototypeOf(current) !== Object.prototype &&\n\t\t\tObject.getPrototypeOf(current) !== null\n\t\t)\n\t\t\tthrow new CursorError()\n\t\tif (Object.getOwnPropertySymbols(current).length) throw new CursorError()\n\t\tconst names = Object.getOwnPropertyNames(current)\n\t\tif (names.length > (array ? bounds.maxArrayLength + 1 : bounds.maxObjectFields))\n\t\t\tthrow new CursorError()\n\t\tancestors.add(current)\n\t\ttry {\n\t\t\tfor (const name of names) {\n\t\t\t\tconst descriptor = Object.getOwnPropertyDescriptor(current, name)!\n\t\t\t\tif (!('value' in descriptor)) throw new CursorError()\n\t\t\t\tif (array && name === 'length') {\n\t\t\t\t\tif (descriptor.value > bounds.maxArrayLength) throw new CursorError()\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\t!descriptor.enumerable ||\n\t\t\t\t\t(array && (!/^(0|[1-9][0-9]*)$/.test(name) || Number(name) >= current.length))\n\t\t\t\t)\n\t\t\t\t\tthrow new CursorError()\n\t\t\t\tvisit(descriptor.value, depth + 1)\n\t\t\t}\n\t\t} finally {\n\t\t\tancestors.delete(current)\n\t\t}\n\t}\n\tvisit(value, 0)\n}\nfunction aad(version: string, keyId: string, binding: CursorBinding): Uint8Array {\n\tcheckDescriptors(binding)\n\tif (binding.clientFilters === undefined || binding.serverFilters === undefined)\n\t\tthrow new CursorError()\n\tconst { table, order, principal, policyVersion, engineVersion, registryVersion } = binding\n\tfor (const field of [table, principal, policyVersion, engineVersion, registryVersion])\n\t\tif (typeof field !== 'string' || !field.length) throw new CursorError()\n\tif (order !== 'asc' && order !== 'desc') throw new CursorError()\n\tfor (const field of [binding.index, binding.parent, binding.tenant])\n\t\tif (field != null && typeof field !== 'string') throw new CursorError()\n\tif (\n\t\t!Array.isArray(binding.relationPath) ||\n\t\tbinding.relationPath.some((part) => typeof part !== 'string')\n\t)\n\t\tthrow new CursorError()\n\treturn encoder.encode(\n\t\tcanonicalConvexBytes(\n\t\t\t{\n\t\t\t\tdomain: 'cvx-query-cursor',\n\t\t\t\tversion,\n\t\t\t\tkeyId,\n\t\t\t\ttable,\n\t\t\t\tindex: binding.index ?? null,\n\t\t\t\torder,\n\t\t\t\tclientFilters: binding.clientFilters,\n\t\t\t\tserverFilters: binding.serverFilters,\n\t\t\t\trelationPath: binding.relationPath,\n\t\t\t\tparent: binding.parent ?? null,\n\t\t\t\tprincipal,\n\t\t\t\ttenant: binding.tenant ?? null,\n\t\t\t\tpolicyVersion,\n\t\t\t\tengineVersion,\n\t\t\t\tregistryVersion,\n\t\t\t},\n\t\t\tbounds,\n\t\t),\n\t)\n}\n"],"mappings":"gMA4BA,IAAaA,EAAb,cAAiC,KAAM,CACtC,KAAgB,cAChB,KAAgB,iBAChB,aAAc,CACb,MAAM,gBAAgB,EACtB,KAAK,KAAO,aACb,CACD,EACaC,EAAb,cAA8C,KAAM,CACnD,KAAgB,2BAChB,aAAc,CACb,MAAM,qCAAqC,EAC3C,KAAK,KAAO,0BACb,CACD,EAEA,MAAMC,EAAS,OAAO,OAAO,CAC5B,SAAU,GACV,SAAU,KACV,SAAU,MACV,eAAgB,IAChB,gBAAiB,GAClB,CAAC,EACKC,EAAoB,KAKpBC,EAAU,IAAI,YACdC,EAAU,IAAI,YAAY,QAAS,CAAE,MAAO,EAAK,CAAC,EAClDC,EAAa,wBAGnB,SAAgBC,GAA+B,CAC9C,GAAI,CACH,IAAMC,EAAU,QAAQ,IAAI,2BACtBC,EAAe,QAAQ,IAAI,gCACjC,GAAI,CAACD,GAAW,CAACC,EAAc,MAAM,IAAIR,EACzC,IAAMS,EAAkB,KAAK,MAAMF,CAAO,EAC1C,GAAuB,OAAOE,GAAW,WAArCA,GAAiD,MAAM,QAAQA,CAAM,EACxE,MAAM,IAAIT,EAQX,OAAOU,EAAkB,CAAE,QAAS,IAAK,eAAc,KAP1C,OAAO,YACnB,OAAO,QAAQD,CAAM,CAAC,CAAC,KAAK,CAACE,EAAIC,KAAS,CACzC,GAAI,OAAOA,GAAQ,UAAY,CAAC,qBAAqB,KAAKA,CAAG,EAC5D,MAAM,IAAIZ,EACX,MAAO,CAACW,EAAIE,EAAWD,CAAG,CAAC,CAC5B,CAAC,CAEwD,CAAE,CAAC,CAC9D,MAAQ,CAEP,MAAM,IAAIZ,CACX,CACD,CAQA,SAAgBU,EAAkB,EAA2B,CAC5D,IAAII,EAAiBN,EACfO,kBAAO,IAAI,IACjB,GAAI,CAGH,GAFA,EAAUC,EAAO,QACjB,EAAeA,EAAO,aAErB,OAAOF,GAAY,UACnB,OAAON,GAAiB,UACxB,CAACH,EAAW,KAAKS,CAAO,GACxB,CAACT,EAAW,KAAKG,CAAY,EAE7B,MAAM,IAAIR,EACX,IAAMiB,EAAU,OAAO,QAAQD,EAAO,IAAI,EAC1C,GAAI,CAACC,EAAQ,OAAQ,MAAM,IAAIjB,EAC/B,IAAK,GAAM,CAACW,EAAIC,KAAQK,EAAS,CAChC,GAAI,CAACZ,EAAW,KAAKM,CAAE,GAAK,EAAEC,aAAe,aAAeA,EAAI,aAAe,GAC9E,MAAM,IAAIZ,EACXe,EAAK,IAAIJ,EAAI,WAAW,KAAKC,CAAG,CAAC,CAClC,CACA,GAAI,CAACG,EAAK,IAAIP,CAAY,EAAG,MAAM,IAAIR,CACxC,MAAQ,CACP,MAAM,IAAIA,CACX,CACA,OAAO,OAAO,OAAO,CACpB,KAAK,EAAa,EAAgC,CACjD,GAAI,CACH,GACC,OAAOkB,GAAQ,UACfA,EAAI,OAAShB,GACbC,EAAQ,OAAOe,CAAG,CAAC,CAAC,OAAShB,EAE7B,MAAM,IAAIH,EACX,IAAMoB,EAAUhB,EAAQ,OAAO,KAAK,UAAUe,CAAG,CAAC,EAC5CE,EAASC,EAAON,EAAK,IAAIP,CAAY,EAAIc,EAAIR,EAASN,EAAce,CAAO,CAAC,EAClF,MAAO,MAAMT,EAAQ,GAAGN,EAAa,GAAGgB,EAAWJ,EAAO,QAAQD,CAAO,CAAC,GAC3E,MAAQ,CACP,MAAM,IAAIpB,CACX,CACD,EACA,KAAK,EAAe,EAAgC,CACnD,GAAI,CACH,GAAI,OAAO0B,GAAU,UAAYA,EAAM,OAAS,MAAgB,MAAM,IAAI1B,EAC1E,IAAM2B,EAAQD,EAAM,MAAM,GAAG,EAC7B,GACCC,EAAM,SAAW,GACjBA,EAAM,KAAO,MACbA,EAAM,KAAOZ,GACb,CAACT,EAAW,KAAKqB,EAAM,EAAG,EAE1B,MAAM,IAAI3B,EACX,IAAM4B,EAAQD,EAAM,GACnBnB,EAAUmB,EAAM,GAChBd,EAAMG,EAAK,IAAIY,CAAK,EACrB,GACC,CAACf,GACDL,EAAQ,OAAS,IACjBA,EAAQ,OAAS,OACjBA,EAAQ,OAAS,GACjB,CAAC,cAAc,KAAKA,CAAO,EAE3B,MAAM,IAAIR,EAEX,IAAM6B,EADSP,EAAOT,EAAKU,EAAIR,EAASa,EAAOJ,CAAO,CAC/B,CAAC,CAAC,QAAQV,EAAWN,CAAO,CAAC,EACpD,GAAIqB,EAAU,OAAS,MAAiB,MAAM,IAAI7B,EAClD,IAAM8B,EAAOzB,EAAQ,OAAOwB,CAAS,EAC/BV,EAAe,KAAK,MAAMW,CAAI,EACpC,GACC,OAAOX,GAAQ,UACfA,EAAI,OAAShB,GACbC,EAAQ,OAAOe,CAAG,CAAC,CAAC,OAAShB,GAC7B,KAAK,UAAUgB,CAAG,IAAMW,EAExB,MAAM,IAAI9B,EACX,OAAOmB,CACR,MAAQ,CACP,MAAM,IAAInB,CACX,CACD,CACD,CAAC,CACF,CAGA,SAAS+B,EAAiB,EAAsB,CAC/C,IAAMC,kBAAY,IAAI,IAClBC,EAAQ,EACZ,SAASC,EAAM,EAAkB,EAAqB,CACrD,GAAI,EAAED,EAAQ/B,EAAO,UAAYiC,EAAQjC,EAAO,SAAU,MAAM,IAAIF,EACpE,GAAwB,OAAOoC,GAAY,WAAvCA,GAAmDA,aAAmB,YAAa,OACvF,GAAIJ,EAAU,IAAII,CAAO,EAAG,MAAM,IAAIpC,EACtC,IAAMqC,EAAQ,MAAM,QAAQD,CAAO,EAQnC,GAPIC,GAAS,OAAO,eAAeD,CAAO,IAAM,MAAM,WAErD,CAACC,GACD,OAAO,eAAeD,CAAO,IAAM,OAAO,WAC1C,OAAO,eAAeA,CAAO,IAAM,MAGhC,OAAO,sBAAsBA,CAAO,CAAC,CAAC,OAAQ,MAAM,IAAIpC,EAC5D,IAAMsC,EAAQ,OAAO,oBAAoBF,CAAO,EAChD,GAAIE,EAAM,QAAUD,EAAQnC,EAAO,eAAiB,EAAIA,EAAO,iBAC9D,MAAM,IAAIF,EACXgC,EAAU,IAAII,CAAO,EACrB,GAAI,CACH,IAAK,IAAMG,KAAQD,EAAO,CACzB,IAAME,EAAa,OAAO,yBAAyBJ,EAASG,CAAI,EAChE,GAAI,EAAE,UAAWC,GAAa,MAAM,IAAIxC,EACxC,GAAIqC,GAASE,IAAS,SAAU,CAC/B,GAAIC,EAAW,MAAQtC,EAAO,eAAgB,MAAM,IAAIF,EACxD,QACD,CACA,GACC,CAACwC,EAAW,YACXH,IAAU,CAAC,oBAAoB,KAAKE,CAAI,GAAK,OAAOA,CAAI,GAAKH,EAAQ,QAEtE,MAAM,IAAIpC,EACXkC,EAAMM,EAAW,MAAOL,EAAQ,CAAC,CAClC,CACD,QAAU,CACTH,EAAU,OAAOI,CAAO,CACzB,CACD,CACAF,EAAMO,EAAO,CAAC,CACf,CACA,SAASlB,EAAI,EAAiB,EAAe,EAAoC,CAEhF,GADAQ,EAAiBP,CAAO,EACpBA,EAAQ,gBAAkB,IAAA,IAAaA,EAAQ,gBAAkB,IAAA,GACpE,MAAM,IAAIxB,EACX,GAAM,CAAE,QAAO,QAAO,YAAW,gBAAe,gBAAe,mBAAoBwB,EACnF,IAAK,IAAMkB,IAAS,CAACC,EAAOC,EAAWC,EAAeC,EAAeC,CAAe,EACnF,GAAI,OAAOL,GAAU,UAAY,CAACA,EAAM,OAAQ,MAAM,IAAI1C,EAC3D,GAAIgD,IAAU,OAASA,IAAU,OAAQ,MAAM,IAAIhD,EACnD,IAAK,IAAM0C,IAAS,CAAClB,EAAQ,MAAOA,EAAQ,OAAQA,EAAQ,MAAM,EACjE,GAAIkB,GAAS,MAAQ,OAAOA,GAAU,SAAU,MAAM,IAAI1C,EAC3D,GACC,CAAC,MAAM,QAAQwB,EAAQ,YAAY,GACnCA,EAAQ,aAAa,KAAM,GAAS,OAAOyB,GAAS,QAAQ,EAE5D,MAAM,IAAIjD,EACX,OAAOI,EAAQ,OACd8C,EACC,CACC,OAAQ,mBACR,UACA,QACA,QACA,MAAO1B,EAAQ,OAAS,KACxB,QACA,cAAeA,EAAQ,cACvB,cAAeA,EAAQ,cACvB,aAAcA,EAAQ,aACtB,OAAQA,EAAQ,QAAU,KAC1B,YACA,OAAQA,EAAQ,QAAU,KAC1B,gBACA,gBACA,iBACD,EACAtB,CACD,CACD,CACD"}
@@ -1,6 +1,6 @@
1
1
  import { ManagedBudgets, ManagedQueryContext, ManagedSecurityConfig } from "./querying/security.mjs";
2
2
  import { IndexCondition, NativePagination } from "./querying/filters.mjs";
3
- import { RelationDeclarations, RelationFilterSelection, RelationFilters, RelationRegistry, RelationRow, RelationSelection, RelationTableHelpers, RelationTables } from "./relations.mjs";
3
+ import { RelationDeclarations, RelationFilterSelection, RelationFilters, RelationRegistry, RelationRow, RelationSelection, RelationTables } from "./relations.mjs";
4
4
  import { CursorCodecConfig } from "./querying/cursors.mjs";
5
5
  import { DataModelFromSchemaDefinition, GenericSchema, PaginationResult, SchemaDefinition } from "convex/server";
6
6
  import { GenericId } from "convex/values";
@@ -21,19 +21,33 @@ export interface TableQuery<Tables extends RelationTables, D extends RelationDec
21
21
  relation<const P extends Paths<D, Extract<T, string>>>(parent: GenericId<Tables[T]['tableName']>, path: P): Omit<TableQuery<Tables, D, Extract<PathTarget<D, Extract<T, string>, P>, keyof Tables>, {}, RemainingDepth<P, L>>, 'relation'>;
22
22
  paginate(args: EngineArgs<Tables, D, T>): Promise<PaginationResult<RelationRow<Tables, D, T, S>>>;
23
23
  }
24
- export interface QueryEngineConfig<Schema extends SchemaDefinition<GenericSchema, boolean>, Tables extends RelationTables, D extends RelationDeclarations> {
24
+ export interface QueryEngineDeclaration<Schema extends SchemaDefinition<GenericSchema, boolean>, Tables extends RelationTables, D extends RelationDeclarations> {
25
25
  schema: Schema;
26
26
  relations: RelationRegistry<Tables, D>;
27
- cursor: CursorCodecConfig;
27
+ }
28
+ /** Advanced server configuration, separate from the normal schema/relations declaration. */
29
+ export interface QueryEngineOptions<Schema extends SchemaDefinition<GenericSchema, boolean>> {
30
+ /** Explicit codecs validate at creation. Otherwise environment keys load at first paginate. */
31
+ cursor?: CursorCodecConfig;
28
32
  budgets?: Partial<ManagedBudgets>;
29
33
  maxPageSize?: number;
30
34
  policyVersion?: string;
31
35
  engineVersion?: string;
32
36
  trustedAdapter?: ManagedSecurityConfig<DataModelFromSchemaDefinition<Schema>>['trustedAdapter'];
33
37
  }
34
- /** Central facade. Endpoint chains carry declarative constraints and explicit relationships. */
35
- export declare function createQueryEngine<Schema extends SchemaDefinition<GenericSchema, boolean>, const Tables extends RelationTables, const D extends RelationDeclarations>(config: QueryEngineConfig<Schema, Tables, D>): ((ctx: ManagedQueryContext<DataModelFromSchemaDefinition<Schema>>) => { [T in keyof Tables]: TableQuery<Tables, D, T>; }) & {
36
- tables: { [T in keyof Tables]: RelationTableHelpers<Tables, D, T>; };
38
+ /** @deprecated Use QueryEngineDeclaration and optional QueryEngineOptions separately. */
39
+ export interface QueryEngineConfig<Schema extends SchemaDefinition<GenericSchema, boolean>, Tables extends RelationTables, D extends RelationDeclarations> extends QueryEngineDeclaration<Schema, Tables, D>, QueryEngineOptions<Schema> {
40
+ cursor: CursorCodecConfig;
41
+ }
42
+ type QueryEngine<Schema extends SchemaDefinition<GenericSchema, boolean>, Tables extends RelationTables, D extends RelationDeclarations> = {
43
+ (ctx: ManagedQueryContext<DataModelFromSchemaDefinition<Schema>>): { [T in keyof Tables]: TableQuery<Tables, D, T>; };
44
+ tables: RelationRegistry<Tables, D>['tables'];
37
45
  };
46
+ /** Central facade. Endpoint chains carry declarative constraints and explicit relationships. */
47
+ export declare function createQueryEngine<Schema extends SchemaDefinition<GenericSchema, boolean>, const Tables extends RelationTables, const D extends RelationDeclarations>(config: QueryEngineDeclaration<Schema, Tables, D>, options?: QueryEngineOptions<Schema>): QueryEngine<Schema, Tables, D>;
48
+ /** @deprecated Pass advanced fields in the optional second argument. */
49
+ export declare function createQueryEngine<Schema extends SchemaDefinition<GenericSchema, boolean>, const Tables extends RelationTables, const D extends RelationDeclarations>(config: QueryEngineDeclaration<Schema, Tables, D> & QueryEngineOptions<Schema> & {
50
+ cursor: CursorCodecConfig;
51
+ }): QueryEngine<Schema, Tables, D>;
38
52
  //#endregion
39
53
  //# sourceMappingURL=querying.d.mts.map
package/dist/querying.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{createManagedSecurity as e}from"./querying/security.mjs";import{createPlanner as t}from"./querying/planner.mjs";import{createCursorCodec as n}from"./querying/cursors.mjs";import{assertRelationshipDepth as r,relationshipExecutor as i}from"./querying/relationships.mjs";import{executeQuery as a}from"./querying/execution.mjs";function o(o){let{schema:s,relations:c}=o,l=t(s,c),u=n(o.cursor),d=o.maxPageSize??100;if(!Number.isSafeInteger(d)||d<=0||d>100)throw Error(`Invalid central page size`);let f=o.engineVersion??`scalar-1`,p=o.policyVersion??`1`,m,h=e({tables:Object.keys(c.tables),policyVersion:p,budgets:o.budgets,trustedAdapter:o.trustedAdapter});function g(e){l.assertUnchanged();let t=h(e);function n(e,o=[],h=[],g={},_){return Object.freeze({when:(t,r)=>n(e,[...o,{value:t,index:r}],h,g,_),where:t=>n(e,o,[...h,t],g,_),with:t=>n(e,o,h,t,_),relation:(t,r)=>{let i=r.split(`.`);if(!i.length||i.length>4)throw Error(`INVALID_RELATION`);let a=e;for(let e of i){let t=l.metadata.declarations[a]?.[e];if(!t)throw Error(`INVALID_RELATION`);a=t.target}return n(a,o,h,{},{table:e,parent:t,path:i})},async paginate(n){l.assertUnchanged();let v=l.page(e,g);r(g,_?.path.length??0);let y=l.plan(e,n,_?[]:o,h);if(y.args.numItems>d)throw Error(`Page size exceeds central ceiling`);y.args.maximumRowsRead=Math.min(y.args.maximumRowsRead??t.budgets.rawCandidates,t.budgets.rawCandidates),y.args.maximumBytesRead=Math.min(y.args.maximumBytesRead??t.budgets.documentBytes,t.budgets.documentBytes),m??=Array.from(new Uint8Array(await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(c.identity))),e=>e.toString(16).padStart(2,`0`)).join(``),l.assertUnchanged();let b={table:_?.table??e,index:y.index?.name??`by_creation_time`,order:y.args.order??`asc`,clientFilters:y.clientIdentity,serverFilters:{predicates:y.serverIdentity,selection:g,scope:{...t.scope}},relationPath:[],principal:JSON.stringify(t.scope),tenant:t.scope.tenant,policyVersion:p,engineVersion:f,registryVersion:m},x=i(s,t,l,u,b);if(_)return x.continuation(_.table,_.parent,_.path,g,y.args,o,h);let S=await a(s,t,e,y,l,u,b,async(t,n)=>!n||!await x.matches(e,t,y.residual)?null:x.output(e,t,g,y.args));return l.assertUnchanged(),v.parse(S)}})}return Object.freeze(Object.fromEntries(Object.keys(c.tables).map(e=>[e,n(e)])))}return Object.assign(g,{tables:c.tables})}export{o as createQueryEngine};
1
+ import{createManagedSecurity as e}from"./querying/security.mjs";import{createPlanner as t}from"./querying/planner.mjs";import{createCursorCodec as n,createEnvironmentCursorCodec as r}from"./querying/cursors.mjs";import{assertRelationshipDepth as i,relationshipExecutor as a}from"./querying/relationships.mjs";import{executeQuery as o}from"./querying/execution.mjs";function s(s,c){let{schema:l,relations:u}=s,d={...s,...c},f=t(l,u),p=d.cursor===void 0?void 0:n(d.cursor),m=d.maxPageSize??100;if(!Number.isSafeInteger(m)||m<=0||m>100)throw Error(`Invalid central page size`);let h=d.engineVersion??`scalar-1`,g=d.policyVersion??`1`,_,v=e({tables:Object.keys(u.tables),policyVersion:g,budgets:d.budgets,trustedAdapter:d.trustedAdapter});function y(e){f.assertUnchanged();let t=v(e);function n(e,s=[],c=[],d={},v){return Object.freeze({when:(t,r)=>n(e,[...s,{value:t,index:r}],c,d,v),where:t=>n(e,s,[...c,t],d,v),with:t=>n(e,s,c,t,v),relation:(t,r)=>{let i=r.split(`.`);if(!i.length||i.length>4)throw Error(`INVALID_RELATION`);let a=e;for(let e of i){let t=f.metadata.declarations[a]?.[e];if(!t)throw Error(`INVALID_RELATION`);a=t.target}return n(a,s,c,{},{table:e,parent:t,path:i})},async paginate(n){let y=p??=r();f.assertUnchanged();let b=f.page(e,d);i(d,v?.path.length??0);let x=f.plan(e,n,v?[]:s,c);if(x.args.numItems>m)throw Error(`Page size exceeds central ceiling`);x.args.maximumRowsRead=Math.min(x.args.maximumRowsRead??t.budgets.rawCandidates,t.budgets.rawCandidates),x.args.maximumBytesRead=Math.min(x.args.maximumBytesRead??t.budgets.documentBytes,t.budgets.documentBytes),_??=Array.from(new Uint8Array(await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(u.identity))),e=>e.toString(16).padStart(2,`0`)).join(``),f.assertUnchanged();let S={table:v?.table??e,index:x.index?.name??`by_creation_time`,order:x.args.order??`asc`,clientFilters:x.clientIdentity,serverFilters:{predicates:x.serverIdentity,selection:d,scope:{...t.scope}},relationPath:[],principal:JSON.stringify(t.scope),tenant:t.scope.tenant,policyVersion:g,engineVersion:h,registryVersion:_},C=a(l,t,f,y,S);if(v)return C.continuation(v.table,v.parent,v.path,d,x.args,s,c);let w=await o(l,t,e,x,f,y,S,async(t,n)=>!n||!await C.matches(e,t,x.residual)?null:C.output(e,t,d,x.args));return f.assertUnchanged(),b.parse(w)}})}return Object.freeze(Object.fromEntries(Object.keys(u.tables).map(e=>[e,n(e)])))}return Object.assign(y,{tables:u.tables})}export{s as createQueryEngine};
2
2
  //# sourceMappingURL=querying.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"querying.mjs","names":["executorRequest","createQueryEngine","config","planner","createPlanner","schema","relations","codec","createCursorCodec","maxPageSize","engineVersion","policyVersion","registryDigest","security","createManagedSecurity","querying","request","ctx","tableQuery","table","conditions","constraints","selection","route","spec","selected","path","declaredPath","target","name","edge","pageSchema","assertRelationshipDepth","plan","input","byte","binding","executor","relationshipExecutor","result","executeQuery","permitted","row"],"sources":["../src/querying.ts"],"sourcesContent":["import type {\n\tGenericSchema,\n\tSchemaDefinition,\n\tDataModelFromSchemaDefinition,\n\tPaginationResult,\n} from 'convex/server'\nimport type { GenericId } from 'convex/values'\nimport type {\n\tRelationTables,\n\tRelationDeclarations,\n\tRelationRegistry,\n\tRelationRow,\n\tRelationSelection,\n\tRelationFilters,\n\tRelationFilterSelection,\n} from './relations'\nimport { createPlanner, type QueryInput, type StoredConstraint } from './querying/planner'\nimport { executeQuery, type ExecutionRequest } from './querying/execution'\nimport {\n\trelationshipExecutor,\n\tassertRelationshipDepth,\n\ttype OutputSelection,\n\ttype RelationDocument,\n} from './querying/relationships'\nimport { createCursorCodec, type CursorCodecConfig, type CursorBinding } from './querying/cursors'\nimport {\n\tcreateManagedSecurity,\n\ttype ManagedSecurityConfig,\n\ttype ManagedQueryContext,\n\ttype ManagedBudgets,\n} from './querying/security'\nimport type { IndexCondition, NativePagination } from './querying/filters'\n\ntype FullMask<S> = {\n\t[K in keyof S]-?: NonNullable<S[K]> extends true ? true : FullMask<NonNullable<S[K]>>\n}\ntype Paths<\n\tD extends RelationDeclarations,\n\tT extends string,\n\tL extends readonly 1[] = [1, 1, 1, 1],\n> = L extends readonly [1, ...infer Rest extends 1[]]\n\t? {\n\t\t\t[K in keyof D[T] & string]:\n\t\t\t\t| (D[T][K]['cardinality'] extends 'many' ? K : never)\n\t\t\t\t| `${K}.${Paths<D, D[T][K]['target'], Rest>}`\n\t\t}[keyof D[T] & string]\n\t: never\ntype PathTarget<\n\tD extends RelationDeclarations,\n\tT extends string,\n\tP extends string,\n> = P extends `${infer Head}.${infer Rest}`\n\t? PathTarget<D, D[T][Head]['target'], Rest>\n\t: D[T][P]['target']\ntype RemainingDepth<P extends string, L extends readonly 1[]> = L extends readonly [\n\t1,\n\t...infer Rest extends 1[],\n]\n\t? P extends `${string}.${infer Tail}`\n\t\t? RemainingDepth<Tail, Rest>\n\t\t: Rest\n\t: []\ntype EngineArgs<\n\tTables extends RelationTables,\n\tD extends RelationDeclarations,\n\tT extends keyof Tables,\n> = NativePagination & {\n\tfilters?: RelationFilters<Tables, D, T, FullMask<RelationFilterSelection<Tables, D, T>>>\n\torder?: 'asc' | 'desc'\n}\n/** Typed declarative chain returned by the engine; named for consumer declaration emission. */\nexport interface TableQuery<\n\tTables extends RelationTables,\n\tD extends RelationDeclarations,\n\tT extends keyof Tables,\n\tS = {},\n\tL extends readonly 1[] = [1, 1, 1, 1],\n> {\n\twhen(value: IndexCondition['value'], index: string): TableQuery<Tables, D, T, S, L>\n\twhere(spec: NonNullable<EngineArgs<Tables, D, T>['filters']>): TableQuery<Tables, D, T, S, L>\n\twith<const Selection extends RelationSelection<Tables, D, T, L>>(\n\t\tselection: Selection & {\n\t\t\t[K in keyof Selection]: K extends keyof RelationSelection<Tables, D, T, L>\n\t\t\t\t? Selection[K]\n\t\t\t\t: never\n\t\t},\n\t): TableQuery<Tables, D, T, Selection, L>\n\trelation<const P extends Paths<D, Extract<T, string>>>(\n\t\tparent: GenericId<Tables[T]['tableName']>,\n\t\tpath: P,\n\t): Omit<\n\t\tTableQuery<\n\t\t\tTables,\n\t\t\tD,\n\t\t\tExtract<PathTarget<D, Extract<T, string>, P>, keyof Tables>,\n\t\t\t{},\n\t\t\tRemainingDepth<P, L>\n\t\t>,\n\t\t'relation'\n\t>\n\tpaginate(args: EngineArgs<Tables, D, T>): Promise<PaginationResult<RelationRow<Tables, D, T, S>>>\n}\nexport interface QueryEngineConfig<\n\tSchema extends SchemaDefinition<GenericSchema, boolean>,\n\tTables extends RelationTables,\n\tD extends RelationDeclarations,\n> {\n\tschema: Schema\n\trelations: RelationRegistry<Tables, D>\n\tcursor: CursorCodecConfig\n\tbudgets?: Partial<ManagedBudgets>\n\tmaxPageSize?: number\n\tpolicyVersion?: string\n\tengineVersion?: string\n\ttrustedAdapter?: ManagedSecurityConfig<DataModelFromSchemaDefinition<Schema>>['trustedAdapter']\n}\n\n/** Central facade. Endpoint chains carry declarative constraints and explicit relationships. */\nexport function createQueryEngine<\n\tSchema extends SchemaDefinition<GenericSchema, boolean>,\n\tconst Tables extends RelationTables,\n\tconst D extends RelationDeclarations,\n>(config: QueryEngineConfig<Schema, Tables, D>) {\n\tconst { schema, relations } = config\n\tconst planner = createPlanner(schema, relations)\n\tconst codec = createCursorCodec(config.cursor)\n\tconst maxPageSize = config.maxPageSize ?? 100\n\tif (!Number.isSafeInteger(maxPageSize) || maxPageSize <= 0 || maxPageSize > 100)\n\t\tthrow new Error('Invalid central page size')\n\tconst engineVersion = config.engineVersion ?? 'scalar-1'\n\tconst policyVersion = config.policyVersion ?? '1'\n\t// Bind the full immutable identity through a collision-resistant digest so\n\t// real application registries fit the codec's bounded AAD. Live guards still\n\t// inspect current schemas; this caches only the immutable version identity.\n\tlet registryDigest: string | undefined\n\tconst security = createManagedSecurity<DataModelFromSchemaDefinition<Schema>>({\n\t\ttables: Object.keys(relations.tables),\n\t\tpolicyVersion,\n\t\tbudgets: config.budgets,\n\t\ttrustedAdapter: config.trustedAdapter,\n\t})\n\tfunction querying(ctx: ManagedQueryContext<DataModelFromSchemaDefinition<Schema>>) {\n\t\tplanner.assertUnchanged()\n\t\tconst request = security(ctx)\n\t\tfunction tableQuery(\n\t\t\ttable: string,\n\t\t\tconditions: readonly IndexCondition[] = [],\n\t\t\tconstraints: readonly StoredConstraint[] = [],\n\t\t\tselection: OutputSelection = {},\n\t\t\troute?: { table: string; parent: string; path: string[] },\n\t\t) {\n\t\t\treturn Object.freeze({\n\t\t\t\twhen: (value: IndexCondition['value'], index: string) =>\n\t\t\t\t\ttableQuery(table, [...conditions, { value, index }], constraints, selection, route),\n\t\t\t\twhere: (spec: StoredConstraint) =>\n\t\t\t\t\ttableQuery(table, conditions, [...constraints, spec], selection, route),\n\t\t\t\twith: (selected: OutputSelection) =>\n\t\t\t\t\ttableQuery(table, conditions, constraints, selected, route),\n\t\t\t\trelation: (parent: string, declaredPath: string) => {\n\t\t\t\t\tconst path = declaredPath.split('.')\n\t\t\t\t\tif (!path.length || path.length > 4) throw new Error('INVALID_RELATION')\n\t\t\t\t\tlet target = table\n\t\t\t\t\tfor (const name of path) {\n\t\t\t\t\t\tconst edge = planner.metadata.declarations[target]?.[name]\n\t\t\t\t\t\tif (!edge) throw new Error('INVALID_RELATION')\n\t\t\t\t\t\ttarget = edge.target\n\t\t\t\t\t}\n\t\t\t\t\treturn tableQuery(target, conditions, constraints, {}, { table, parent, path })\n\t\t\t\t},\n\t\t\t\tasync paginate(input: QueryInput) {\n\t\t\t\t\tplanner.assertUnchanged()\n\t\t\t\t\tconst pageSchema = planner.page(table, selection)\n\t\t\t\t\tassertRelationshipDepth(selection, route?.path.length ?? 0)\n\t\t\t\t\tconst plan = planner.plan(table, input, route ? [] : conditions, constraints)\n\t\t\t\t\tif (plan.args.numItems > maxPageSize) throw new Error('Page size exceeds central ceiling')\n\t\t\t\t\t// Native root limits stay native; lowering those limits cannot raise\n\t\t\t\t\t// the independent shared request meter.\n\t\t\t\t\tplan.args.maximumRowsRead = Math.min(\n\t\t\t\t\t\tplan.args.maximumRowsRead ?? request.budgets.rawCandidates,\n\t\t\t\t\t\trequest.budgets.rawCandidates,\n\t\t\t\t\t)\n\t\t\t\t\tplan.args.maximumBytesRead = Math.min(\n\t\t\t\t\t\tplan.args.maximumBytesRead ?? request.budgets.documentBytes,\n\t\t\t\t\t\trequest.budgets.documentBytes,\n\t\t\t\t\t)\n\t\t\t\t\tregistryDigest ??= Array.from(\n\t\t\t\t\t\tnew Uint8Array(\n\t\t\t\t\t\t\tawait crypto.subtle.digest('SHA-256', new TextEncoder().encode(relations.identity)),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t(byte) => byte.toString(16).padStart(2, '0'),\n\t\t\t\t\t).join('')\n\t\t\t\t\t// The digest await is a boundary too: mutation must be detected\n\t\t\t\t\t// before authenticated cursor validation or native query construction.\n\t\t\t\t\tplanner.assertUnchanged()\n\t\t\t\t\t// SAFETY: selection has passed the registry page validator before binding.\n\t\t\t\t\tconst binding: CursorBinding = {\n\t\t\t\t\t\ttable: route?.table ?? table,\n\t\t\t\t\t\tindex: plan.index?.name ?? 'by_creation_time',\n\t\t\t\t\t\torder: plan.args.order ?? 'asc',\n\t\t\t\t\t\tclientFilters: plan.clientIdentity,\n\t\t\t\t\t\t// Include output selection and the complete immutable scope in trusted identity.\n\t\t\t\t\t\tserverFilters: {\n\t\t\t\t\t\t\tpredicates: plan.serverIdentity,\n\t\t\t\t\t\t\tselection: selection as import('convex/values').Value,\n\t\t\t\t\t\t\tscope: { ...request.scope },\n\t\t\t\t\t\t},\n\t\t\t\t\t\trelationPath: [],\n\t\t\t\t\t\tprincipal: JSON.stringify(request.scope),\n\t\t\t\t\t\ttenant: request.scope.tenant,\n\t\t\t\t\t\tpolicyVersion,\n\t\t\t\t\t\tengineVersion,\n\t\t\t\t\t\tregistryVersion: registryDigest,\n\t\t\t\t\t}\n\t\t\t\t\t// The executor accepts registry-selected native tables, rather than a static DM key.\n\t\t\t\t\tconst executorRequest: unknown = request\n\t\t\t\t\t// SAFETY: only native DM types are erased at this private executor boundary.\n\t\t\t\t\tconst executor = relationshipExecutor(\n\t\t\t\t\t\tschema,\n\t\t\t\t\t\texecutorRequest as ExecutionRequest,\n\t\t\t\t\t\tplanner,\n\t\t\t\t\t\tcodec,\n\t\t\t\t\t\tbinding,\n\t\t\t\t\t)\n\t\t\t\t\tif (route)\n\t\t\t\t\t\treturn executor.continuation(\n\t\t\t\t\t\t\troute.table,\n\t\t\t\t\t\t\troute.parent,\n\t\t\t\t\t\t\troute.path,\n\t\t\t\t\t\t\tselection,\n\t\t\t\t\t\t\tplan.args,\n\t\t\t\t\t\t\tconditions,\n\t\t\t\t\t\t\tconstraints,\n\t\t\t\t\t\t)\n\t\t\t\t\tconst result = await executeQuery(\n\t\t\t\t\t\tschema,\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\ttable,\n\t\t\t\t\t\tplan,\n\t\t\t\t\t\tplanner,\n\t\t\t\t\t\tcodec,\n\t\t\t\t\t\tbinding,\n\t\t\t\t\t\tasync (row, permitted) => {\n\t\t\t\t\t\t\t// SAFETY: metered native stream rows contain the validated system fields.\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t!permitted ||\n\t\t\t\t\t\t\t\t!(await executor.matches(table, row as RelationDocument, plan.residual))\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t\t// SAFETY: the same native row is authorized and matches the declared predicates.\n\t\t\t\t\t\t\treturn executor.output(table, row as RelationDocument, selection, plan.args)\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t\tplanner.assertUnchanged()\n\t\t\t\t\treturn pageSchema.parse(result)\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t\t// SAFETY: each key comes from the exact validated registry; the planner\n\t\t// validates all input and projection while mapped types retain table brands.\n\t\treturn Object.freeze(\n\t\t\tObject.fromEntries(Object.keys(relations.tables).map((table) => [table, tableQuery(table)])),\n\t\t) as { [T in keyof Tables]: TableQuery<Tables, D, T> }\n\t}\n\treturn Object.assign(querying, { tables: relations.tables })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"querying.mjs","names":["executorRequest","createQueryEngine","config","settings","options","planner","createPlanner","schema","relations","cursorCodec","createCursorCodec","maxPageSize","engineVersion","policyVersion","registryDigest","security","createManagedSecurity","querying","request","ctx","tableQuery","table","conditions","constraints","selection","route","spec","selected","path","declaredPath","target","name","edge","codec","createEnvironmentCursorCodec","pageSchema","assertRelationshipDepth","plan","input","byte","binding","executor","relationshipExecutor","result","executeQuery","permitted","row"],"sources":["../src/querying.ts"],"sourcesContent":["import type {\n\tGenericSchema,\n\tSchemaDefinition,\n\tDataModelFromSchemaDefinition,\n\tPaginationResult,\n} from 'convex/server'\nimport type { GenericId } from 'convex/values'\nimport type {\n\tRelationTables,\n\tRelationDeclarations,\n\tRelationRegistry,\n\tRelationRow,\n\tRelationSelection,\n\tRelationFilters,\n\tRelationFilterSelection,\n} from './relations'\nimport { createPlanner, type QueryInput, type StoredConstraint } from './querying/planner'\nimport { executeQuery, type ExecutionRequest } from './querying/execution'\nimport {\n\trelationshipExecutor,\n\tassertRelationshipDepth,\n\ttype OutputSelection,\n\ttype RelationDocument,\n} from './querying/relationships'\nimport {\n\tcreateCursorCodec,\n\tcreateEnvironmentCursorCodec,\n\ttype CursorCodecConfig,\n\ttype CursorBinding,\n} from './querying/cursors'\nimport {\n\tcreateManagedSecurity,\n\ttype ManagedSecurityConfig,\n\ttype ManagedQueryContext,\n\ttype ManagedBudgets,\n} from './querying/security'\nimport type { IndexCondition, NativePagination } from './querying/filters'\n\ntype FullMask<S> = {\n\t[K in keyof S]-?: NonNullable<S[K]> extends true ? true : FullMask<NonNullable<S[K]>>\n}\ntype Paths<\n\tD extends RelationDeclarations,\n\tT extends string,\n\tL extends readonly 1[] = [1, 1, 1, 1],\n> = L extends readonly [1, ...infer Rest extends 1[]]\n\t? {\n\t\t\t[K in keyof D[T] & string]:\n\t\t\t\t| (D[T][K]['cardinality'] extends 'many' ? K : never)\n\t\t\t\t| `${K}.${Paths<D, D[T][K]['target'], Rest>}`\n\t\t}[keyof D[T] & string]\n\t: never\ntype PathTarget<\n\tD extends RelationDeclarations,\n\tT extends string,\n\tP extends string,\n> = P extends `${infer Head}.${infer Rest}`\n\t? PathTarget<D, D[T][Head]['target'], Rest>\n\t: D[T][P]['target']\ntype RemainingDepth<P extends string, L extends readonly 1[]> = L extends readonly [\n\t1,\n\t...infer Rest extends 1[],\n]\n\t? P extends `${string}.${infer Tail}`\n\t\t? RemainingDepth<Tail, Rest>\n\t\t: Rest\n\t: []\ntype EngineArgs<\n\tTables extends RelationTables,\n\tD extends RelationDeclarations,\n\tT extends keyof Tables,\n> = NativePagination & {\n\tfilters?: RelationFilters<Tables, D, T, FullMask<RelationFilterSelection<Tables, D, T>>>\n\torder?: 'asc' | 'desc'\n}\n/** Typed declarative chain returned by the engine; named for consumer declaration emission. */\nexport interface TableQuery<\n\tTables extends RelationTables,\n\tD extends RelationDeclarations,\n\tT extends keyof Tables,\n\tS = {},\n\tL extends readonly 1[] = [1, 1, 1, 1],\n> {\n\twhen(value: IndexCondition['value'], index: string): TableQuery<Tables, D, T, S, L>\n\twhere(spec: NonNullable<EngineArgs<Tables, D, T>['filters']>): TableQuery<Tables, D, T, S, L>\n\twith<const Selection extends RelationSelection<Tables, D, T, L>>(\n\t\tselection: Selection & {\n\t\t\t[K in keyof Selection]: K extends keyof RelationSelection<Tables, D, T, L>\n\t\t\t\t? Selection[K]\n\t\t\t\t: never\n\t\t},\n\t): TableQuery<Tables, D, T, Selection, L>\n\trelation<const P extends Paths<D, Extract<T, string>>>(\n\t\tparent: GenericId<Tables[T]['tableName']>,\n\t\tpath: P,\n\t): Omit<\n\t\tTableQuery<\n\t\t\tTables,\n\t\t\tD,\n\t\t\tExtract<PathTarget<D, Extract<T, string>, P>, keyof Tables>,\n\t\t\t{},\n\t\t\tRemainingDepth<P, L>\n\t\t>,\n\t\t'relation'\n\t>\n\tpaginate(args: EngineArgs<Tables, D, T>): Promise<PaginationResult<RelationRow<Tables, D, T, S>>>\n}\nexport interface QueryEngineDeclaration<\n\tSchema extends SchemaDefinition<GenericSchema, boolean>,\n\tTables extends RelationTables,\n\tD extends RelationDeclarations,\n> {\n\tschema: Schema\n\trelations: RelationRegistry<Tables, D>\n}\n/** Advanced server configuration, separate from the normal schema/relations declaration. */\nexport interface QueryEngineOptions<Schema extends SchemaDefinition<GenericSchema, boolean>> {\n\t/** Explicit codecs validate at creation. Otherwise environment keys load at first paginate. */\n\tcursor?: CursorCodecConfig\n\tbudgets?: Partial<ManagedBudgets>\n\tmaxPageSize?: number\n\tpolicyVersion?: string\n\tengineVersion?: string\n\ttrustedAdapter?: ManagedSecurityConfig<DataModelFromSchemaDefinition<Schema>>['trustedAdapter']\n}\n\n/** @deprecated Use QueryEngineDeclaration and optional QueryEngineOptions separately. */\nexport interface QueryEngineConfig<\n\tSchema extends SchemaDefinition<GenericSchema, boolean>,\n\tTables extends RelationTables,\n\tD extends RelationDeclarations,\n>\n\textends QueryEngineDeclaration<Schema, Tables, D>, QueryEngineOptions<Schema> {\n\tcursor: CursorCodecConfig\n}\n\ntype QueryEngine<\n\tSchema extends SchemaDefinition<GenericSchema, boolean>,\n\tTables extends RelationTables,\n\tD extends RelationDeclarations,\n> = {\n\t(ctx: ManagedQueryContext<DataModelFromSchemaDefinition<Schema>>): {\n\t\t[T in keyof Tables]: TableQuery<Tables, D, T>\n\t}\n\ttables: RelationRegistry<Tables, D>['tables']\n}\n\n/** Central facade. Endpoint chains carry declarative constraints and explicit relationships. */\nexport function createQueryEngine<\n\tSchema extends SchemaDefinition<GenericSchema, boolean>,\n\tconst Tables extends RelationTables,\n\tconst D extends RelationDeclarations,\n>(\n\tconfig: QueryEngineDeclaration<Schema, Tables, D>,\n\toptions?: QueryEngineOptions<Schema>,\n): QueryEngine<Schema, Tables, D>\n/** @deprecated Pass advanced fields in the optional second argument. */\nexport function createQueryEngine<\n\tSchema extends SchemaDefinition<GenericSchema, boolean>,\n\tconst Tables extends RelationTables,\n\tconst D extends RelationDeclarations,\n>(\n\tconfig: QueryEngineDeclaration<Schema, Tables, D> &\n\t\tQueryEngineOptions<Schema> & { cursor: CursorCodecConfig },\n): QueryEngine<Schema, Tables, D>\nexport function createQueryEngine<\n\tSchema extends SchemaDefinition<GenericSchema, boolean>,\n\tconst Tables extends RelationTables,\n\tconst D extends RelationDeclarations,\n>(\n\tconfig: QueryEngineDeclaration<Schema, Tables, D> & QueryEngineOptions<Schema>,\n\toptions?: QueryEngineOptions<Schema>,\n) {\n\tconst { schema, relations } = config\n\t// Retain released one-argument calls; a second argument overrides legacy fields.\n\tconst settings = { ...config, ...options }\n\tconst planner = createPlanner(schema, relations)\n\tlet cursorCodec = settings.cursor === undefined ? undefined : createCursorCodec(settings.cursor)\n\tconst maxPageSize = settings.maxPageSize ?? 100\n\tif (!Number.isSafeInteger(maxPageSize) || maxPageSize <= 0 || maxPageSize > 100)\n\t\tthrow new Error('Invalid central page size')\n\tconst engineVersion = settings.engineVersion ?? 'scalar-1'\n\tconst policyVersion = settings.policyVersion ?? '1'\n\t// Bind the full immutable identity through a collision-resistant digest so\n\t// real application registries fit the codec's bounded AAD. Live guards still\n\t// inspect current schemas; this caches only the immutable version identity.\n\tlet registryDigest: string | undefined\n\tconst security = createManagedSecurity<DataModelFromSchemaDefinition<Schema>>({\n\t\ttables: Object.keys(relations.tables),\n\t\tpolicyVersion,\n\t\tbudgets: settings.budgets,\n\t\ttrustedAdapter: settings.trustedAdapter,\n\t})\n\tfunction querying(ctx: ManagedQueryContext<DataModelFromSchemaDefinition<Schema>>) {\n\t\tplanner.assertUnchanged()\n\t\tconst request = security(ctx)\n\t\tfunction tableQuery(\n\t\t\ttable: string,\n\t\t\tconditions: readonly IndexCondition[] = [],\n\t\t\tconstraints: readonly StoredConstraint[] = [],\n\t\t\tselection: OutputSelection = {},\n\t\t\troute?: { table: string; parent: string; path: string[] },\n\t\t) {\n\t\t\treturn Object.freeze({\n\t\t\t\twhen: (value: IndexCondition['value'], index: string) =>\n\t\t\t\t\ttableQuery(table, [...conditions, { value, index }], constraints, selection, route),\n\t\t\t\twhere: (spec: StoredConstraint) =>\n\t\t\t\t\ttableQuery(table, conditions, [...constraints, spec], selection, route),\n\t\t\t\twith: (selected: OutputSelection) =>\n\t\t\t\t\ttableQuery(table, conditions, constraints, selected, route),\n\t\t\t\trelation: (parent: string, declaredPath: string) => {\n\t\t\t\t\tconst path = declaredPath.split('.')\n\t\t\t\t\tif (!path.length || path.length > 4) throw new Error('INVALID_RELATION')\n\t\t\t\t\tlet target = table\n\t\t\t\t\tfor (const name of path) {\n\t\t\t\t\t\tconst edge = planner.metadata.declarations[target]?.[name]\n\t\t\t\t\t\tif (!edge) throw new Error('INVALID_RELATION')\n\t\t\t\t\t\ttarget = edge.target\n\t\t\t\t\t}\n\t\t\t\t\treturn tableQuery(target, conditions, constraints, {}, { table, parent, path })\n\t\t\t\t},\n\t\t\t\tasync paginate(input: QueryInput) {\n\t\t\t\t\t// Resolve once per global engine, before any native query or policy reads.\n\t\t\t\t\tconst codec = (cursorCodec ??= createEnvironmentCursorCodec())\n\t\t\t\t\tplanner.assertUnchanged()\n\t\t\t\t\tconst pageSchema = planner.page(table, selection)\n\t\t\t\t\tassertRelationshipDepth(selection, route?.path.length ?? 0)\n\t\t\t\t\tconst plan = planner.plan(table, input, route ? [] : conditions, constraints)\n\t\t\t\t\tif (plan.args.numItems > maxPageSize) throw new Error('Page size exceeds central ceiling')\n\t\t\t\t\t// Native root limits stay native; lowering those limits cannot raise\n\t\t\t\t\t// the independent shared request meter.\n\t\t\t\t\tplan.args.maximumRowsRead = Math.min(\n\t\t\t\t\t\tplan.args.maximumRowsRead ?? request.budgets.rawCandidates,\n\t\t\t\t\t\trequest.budgets.rawCandidates,\n\t\t\t\t\t)\n\t\t\t\t\tplan.args.maximumBytesRead = Math.min(\n\t\t\t\t\t\tplan.args.maximumBytesRead ?? request.budgets.documentBytes,\n\t\t\t\t\t\trequest.budgets.documentBytes,\n\t\t\t\t\t)\n\t\t\t\t\tregistryDigest ??= Array.from(\n\t\t\t\t\t\tnew Uint8Array(\n\t\t\t\t\t\t\tawait crypto.subtle.digest('SHA-256', new TextEncoder().encode(relations.identity)),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t(byte) => byte.toString(16).padStart(2, '0'),\n\t\t\t\t\t).join('')\n\t\t\t\t\t// The digest await is a boundary too: mutation must be detected\n\t\t\t\t\t// before authenticated cursor validation or native query construction.\n\t\t\t\t\tplanner.assertUnchanged()\n\t\t\t\t\t// SAFETY: selection has passed the registry page validator before binding.\n\t\t\t\t\tconst binding: CursorBinding = {\n\t\t\t\t\t\ttable: route?.table ?? table,\n\t\t\t\t\t\tindex: plan.index?.name ?? 'by_creation_time',\n\t\t\t\t\t\torder: plan.args.order ?? 'asc',\n\t\t\t\t\t\tclientFilters: plan.clientIdentity,\n\t\t\t\t\t\t// Include output selection and the complete immutable scope in trusted identity.\n\t\t\t\t\t\tserverFilters: {\n\t\t\t\t\t\t\tpredicates: plan.serverIdentity,\n\t\t\t\t\t\t\tselection: selection as import('convex/values').Value,\n\t\t\t\t\t\t\tscope: { ...request.scope },\n\t\t\t\t\t\t},\n\t\t\t\t\t\trelationPath: [],\n\t\t\t\t\t\tprincipal: JSON.stringify(request.scope),\n\t\t\t\t\t\ttenant: request.scope.tenant,\n\t\t\t\t\t\tpolicyVersion,\n\t\t\t\t\t\tengineVersion,\n\t\t\t\t\t\tregistryVersion: registryDigest,\n\t\t\t\t\t}\n\t\t\t\t\t// The executor accepts registry-selected native tables, rather than a static DM key.\n\t\t\t\t\tconst executorRequest: unknown = request\n\t\t\t\t\t// SAFETY: only native DM types are erased at this private executor boundary.\n\t\t\t\t\tconst executor = relationshipExecutor(\n\t\t\t\t\t\tschema,\n\t\t\t\t\t\texecutorRequest as ExecutionRequest,\n\t\t\t\t\t\tplanner,\n\t\t\t\t\t\tcodec,\n\t\t\t\t\t\tbinding,\n\t\t\t\t\t)\n\t\t\t\t\tif (route)\n\t\t\t\t\t\treturn executor.continuation(\n\t\t\t\t\t\t\troute.table,\n\t\t\t\t\t\t\troute.parent,\n\t\t\t\t\t\t\troute.path,\n\t\t\t\t\t\t\tselection,\n\t\t\t\t\t\t\tplan.args,\n\t\t\t\t\t\t\tconditions,\n\t\t\t\t\t\t\tconstraints,\n\t\t\t\t\t\t)\n\t\t\t\t\tconst result = await executeQuery(\n\t\t\t\t\t\tschema,\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\ttable,\n\t\t\t\t\t\tplan,\n\t\t\t\t\t\tplanner,\n\t\t\t\t\t\tcodec,\n\t\t\t\t\t\tbinding,\n\t\t\t\t\t\tasync (row, permitted) => {\n\t\t\t\t\t\t\t// SAFETY: metered native stream rows contain the validated system fields.\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t!permitted ||\n\t\t\t\t\t\t\t\t!(await executor.matches(table, row as RelationDocument, plan.residual))\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t\t// SAFETY: the same native row is authorized and matches the declared predicates.\n\t\t\t\t\t\t\treturn executor.output(table, row as RelationDocument, selection, plan.args)\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t\tplanner.assertUnchanged()\n\t\t\t\t\treturn pageSchema.parse(result)\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t\t// SAFETY: each key comes from the exact validated registry; the planner\n\t\t// validates all input and projection while mapped types retain table brands.\n\t\treturn Object.freeze(\n\t\t\tObject.fromEntries(Object.keys(relations.tables).map((table) => [table, tableQuery(table)])),\n\t\t) as { [T in keyof Tables]: TableQuery<Tables, D, T> }\n\t}\n\treturn Object.assign(querying, { tables: relations.tables })\n}\n"],"mappings":"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"}
package/docs/oxlint.md CHANGED
@@ -178,7 +178,13 @@ predicate can change authorization, ordering, and continuation semantics.
178
178
 
179
179
  `root-facade-ownership` requires `defineRelations` in `convex/relations.ts` and
180
180
  `createQueryEngine` in `convex/querying.ts`, each configured once. The same rule
181
- continues to enforce auth, triggers, and foundation facades.
181
+ continues to enforce auth, triggers, and foundation facades. Declare one global
182
+ engine as `createQueryEngine({ schema, relations })`, then bind it with
183
+ `querying(ctx)` inside handlers. Avoid rebuilding engines per request or table.
184
+ Cursor key parsing belongs to the engine's server environment loader; advanced
185
+ application-wide overrides use the optional second argument. This setup guidance
186
+ is a convention; the rule currently checks facade ownership and repeated factory
187
+ calls, rather than proving module-scope placement or environment parsing.
182
188
 
183
189
  `root-wiring-only` and the whole-project checker permit these root files only
184
190
  as narrow declarative factory configurations. `relations.ts` can import domain
package/docs/querying.md CHANGED
@@ -100,9 +100,8 @@ indexes. Convex does not infer relationships; this registry supplies them.
100
100
  import { createQueryEngine } from 'cvx-kit/querying'
101
101
  import schema from './schema'
102
102
  import { relations } from './relations'
103
- import { cursor } from './functions'
104
103
 
105
- export const querying = createQueryEngine({ schema, relations, cursor })
104
+ export const querying = createQueryEngine({ schema, relations })
106
105
  ```
107
106
 
108
107
  The application's `functions.ts` facade configures `createAuthFunctions`,
@@ -118,12 +117,24 @@ contexts are unsupported. A host with another auth system can centrally configur
118
117
  `trustedAdapter`; it must attest the query reader, effective policy, and security
119
118
  scope. Do not expose adapter selection to endpoint arguments.
120
119
 
121
- `cursor` has `{ version, currentKeyId, keys }`. `keys` maps key IDs to server-only
122
- 64-byte `Uint8Array` secrets; `currentKeyId` names the key used for new tokens.
123
- Load real secrets through server configuration, never from clients or a constant
124
- fixture key. Keep prior keys during rotation to preserve existing cursors.
125
- The default codec uses authenticated AES-SIV encryption; there is no plaintext
126
- fallback. Identical bound payloads reveal equality.
120
+ Declare this engine once at module scope. It serves every registered table;
121
+ `querying(ctx)` binds authorization and read accounting to the current request.
122
+ Table fields, indexes, DTOs, and relationship metadata come from the schema and
123
+ relation registry. No per-table engine declaration is needed.
124
+
125
+ The engine loads cursor keys internally when pagination first runs. Set
126
+ `CVX_QUERY_CURSOR_KEYS_JSON` to a JSON object mapping key IDs to 128 hexadecimal
127
+ characters (64 secret bytes), and `CVX_QUERY_CURSOR_CURRENT_KEY_ID` to the ID
128
+ used for new tokens. These are server environment variables, shared by all
129
+ tables. Generate real secrets outside query execution and configure them through
130
+ Convex environment settings; never put them in client arguments, source, or logs.
131
+ Keep prior keys during rotation to preserve existing cursors. Changing environment
132
+ configuration requires the engine to reload with the new deployment configuration.
133
+
134
+ Engine declaration and auth registration do not read these variables. Missing or
135
+ invalid configuration fails pagination before database reads. The codec's format
136
+ version is internal. It uses authenticated AES-SIV encryption with no plaintext
137
+ fallback; identical bound payloads reveal equality.
127
138
 
128
139
  ## Define input and result contracts
129
140
 
@@ -249,10 +260,35 @@ changes are rechecked on resume. Native `endCursor` supports bounded partitions.
249
260
 
250
261
  ## Central limits and operational behavior
251
262
 
252
- `createQueryEngine` also accepts `maxPageSize`, `budgets`, `policyVersion`,
253
- `engineVersion`, and `trustedAdapter`. These belong in central configuration,
254
- not per-endpoint filter/project callbacks. Page sizes are positive integers up
255
- to 100; `maxPageSize` can lower that ceiling. Shared request budgets cover read
263
+ The engine supplies default limits and cursor compatibility versions. Normal
264
+ applications need only `{ schema, relations }`. Optional central overrides belong
265
+ in the second argument:
266
+
267
+ ```ts
268
+ export const querying = createQueryEngine(
269
+ { schema, relations },
270
+ {
271
+ maxPageSize: 50,
272
+ },
273
+ )
274
+ ```
275
+
276
+ | Option | Purpose | Default setup |
277
+ | ---------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------- |
278
+ | `budgets` | Lower shared read, byte, fan-out, depth, and memo ceilings | Engine defaults |
279
+ | `maxPageSize` | Lower the 100-item page ceiling | 100 |
280
+ | `policyVersion` | Invalidate old cursors after a policy semantic change | Internal default; current authorization is always rechecked |
281
+ | `engineVersion` | Override the engine compatibility identity | Internal default |
282
+ | `trustedAdapter` | Connect another auth system's query reader and policies | Managed auth capability from `cvx-kit/auth` |
283
+ | `cursor` | Supply an explicit key ring, for custom secret management or migration | Server environment loader |
284
+
285
+ These are application-wide overrides, never endpoint arguments. Existing
286
+ one-argument declarations with these fields still work for compatibility; move
287
+ those fields to the second argument when migrating. To preserve pre-migration
288
+ cursors, keep the existing explicit cursor version and key ring until those
289
+ cursors have expired.
290
+
291
+ Page sizes are positive integers up to 100. Shared request budgets cover read
256
292
  operations, index ranges, raw candidates, document bytes, fan-out, traversal
257
293
  depth, and memo entries, including managed policy reads. Native request read
258
294
  limits may lower ceilings. Output relationship depth is at most four.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cvx-kit",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Reusable Convex application kit: foundation kernels, approvals component, zod table boundaries, WorkOS-backed auth function constructors, and audited command protocol.",
5
5
  "keywords": [
6
6
  "component",
@@ -54,8 +54,7 @@ writeFileSync(
54
54
  `import { createQueryEngine } from 'cvx-kit/querying'
55
55
  import schema from './schema'
56
56
  import { relations } from './relations'
57
- import { cursor } from './functions'
58
- export const querying = createQueryEngine({ schema, relations, cursor })
57
+ export const querying = createQueryEngine({ schema, relations })
59
58
  `,
60
59
  )
61
60
  const originalRelations = readFileSync(join(fixture, 'convex/relations.ts'), 'utf8')
@@ -75,10 +74,9 @@ import { authQuery } from '../functions'
75
74
  import { relations as equivalent } from '../relationsEquivalent'
76
75
  import { relations as changed } from '../relationsChanged'
77
76
  import schema from '../schema'
78
- import { cursor } from '../functions'
79
77
  import { contract } from '../domain/tasks/contracts'
80
- const eq = createQueryEngine({ schema, relations: equivalent, cursor })
81
- const different = createQueryEngine({ schema, relations: changed, cursor })
78
+ const eq = createQueryEngine({ schema, relations: equivalent })
79
+ const different = createQueryEngine({ schema, relations: changed })
82
80
  export const resume = authQuery({ args: contract.queries.list.input, returns: contract.queries.list.result, handler: (ctx,args) => eq(ctx).tasks.paginate(args) })
83
81
  export const reject = authQuery({ args: contract.queries.list.input, returns: contract.queries.list.result, handler: (ctx,args) => different(ctx).tasks.paginate(args) })
84
82
  `,
@@ -125,6 +123,10 @@ for(const [name, fn] of Object.entries(api)) writeFileSync('validator-'+name+'.j
125
123
  writeFileSync(
126
124
  join(fixture, 'verify.mjs'),
127
125
  `import assert from 'node:assert/strict';import {readFileSync,writeFileSync} from 'node:fs';import {spawnSync} from 'node:child_process';
126
+ // Public fixture key only: configure the isolated backend, exercising its real process.env.
127
+ for (const [name,value] of [['CVX_QUERY_CURSOR_KEYS_JSON',JSON.stringify({fixture:'07'.repeat(64)})],['CVX_QUERY_CURSOR_CURRENT_KEY_ID','fixture']]) {
128
+ const result=spawnSync(process.execPath,['node_modules/convex/bin/main.js','env','set',name,value],{encoding:'utf8',timeout:30000});assert.equal(result.status,0,'fixture env setup failed');
129
+ }
128
130
  const key=JSON.parse(readFileSync('.convex/local/default/config.json')).adminKey;
129
131
  const who={subject:'native-reader',issuer:'https://fixture.test',tokenIdentifier:'https://fixture.test|native-reader',org_id:'a',role:'reader'};
130
132
  const url='http://127.0.0.1:'+process.argv[2];const records=[];