@hypequery/protocol 0.20.0 → 0.21.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.
Files changed (62) hide show
  1. package/README.md +17 -0
  2. package/dist/bundles/codec.js +2 -2
  3. package/dist/cache-keys/cache-keys.js +2 -2
  4. package/dist/cache-preimages/cache-preimages.js +3 -3
  5. package/dist/client-contract/bounds.d.ts +3 -0
  6. package/dist/client-contract/bounds.d.ts.map +1 -0
  7. package/dist/client-contract/bounds.js +24 -0
  8. package/dist/client-contract/codec.d.ts +4 -0
  9. package/dist/client-contract/codec.d.ts.map +1 -0
  10. package/dist/client-contract/codec.js +8 -0
  11. package/dist/client-contract/decode.d.ts +4 -0
  12. package/dist/client-contract/decode.d.ts.map +1 -0
  13. package/dist/client-contract/decode.js +100 -0
  14. package/dist/client-contract/errors.d.ts +21 -0
  15. package/dist/client-contract/errors.d.ts.map +1 -0
  16. package/dist/client-contract/errors.js +26 -0
  17. package/dist/client-contract/index.d.ts +8 -0
  18. package/dist/client-contract/index.d.ts.map +1 -0
  19. package/dist/client-contract/index.js +4 -0
  20. package/dist/client-contract/nodes.d.ts +8 -0
  21. package/dist/client-contract/nodes.d.ts.map +1 -0
  22. package/dist/client-contract/nodes.js +68 -0
  23. package/dist/client-contract/primitives.d.ts +15 -0
  24. package/dist/client-contract/primitives.d.ts.map +1 -0
  25. package/dist/client-contract/primitives.js +74 -0
  26. package/dist/client-contract/query.d.ts +10 -0
  27. package/dist/client-contract/query.d.ts.map +1 -0
  28. package/dist/client-contract/query.js +121 -0
  29. package/dist/client-contract/safe-data.d.ts +6 -0
  30. package/dist/client-contract/safe-data.d.ts.map +1 -0
  31. package/dist/client-contract/safe-data.js +124 -0
  32. package/dist/client-contract/timezone-names.d.ts +2 -0
  33. package/dist/client-contract/timezone-names.d.ts.map +1 -0
  34. package/dist/client-contract/timezone-names.js +600 -0
  35. package/dist/client-contract/types.d.ts +68 -0
  36. package/dist/client-contract/types.d.ts.map +1 -0
  37. package/dist/client-contract/types.js +1 -0
  38. package/dist/client-contract/validate.d.ts +6 -0
  39. package/dist/client-contract/validate.d.ts.map +1 -0
  40. package/dist/client-contract/validate.js +95 -0
  41. package/dist/deployments/codec.js +2 -2
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +2 -0
  45. package/dist/releases/codec.js +2 -2
  46. package/dist/values/codec.js +2 -2
  47. package/dist/visibility/errors.d.ts +8 -0
  48. package/dist/visibility/errors.d.ts.map +1 -0
  49. package/dist/visibility/errors.js +13 -0
  50. package/dist/visibility/index.d.ts +5 -0
  51. package/dist/visibility/index.d.ts.map +1 -0
  52. package/dist/visibility/index.js +3 -0
  53. package/dist/visibility/resolve.d.ts +30 -0
  54. package/dist/visibility/resolve.d.ts.map +1 -0
  55. package/dist/visibility/resolve.js +108 -0
  56. package/dist/visibility/types.d.ts +32 -0
  57. package/dist/visibility/types.d.ts.map +1 -0
  58. package/dist/visibility/types.js +1 -0
  59. package/dist/visibility/validate.d.ts +11 -0
  60. package/dist/visibility/validate.d.ts.map +1 -0
  61. package/dist/visibility/validate.js +78 -0
  62. package/package.json +3 -3
package/README.md CHANGED
@@ -19,6 +19,7 @@ This package implements those rules in TypeScript. Python and other implementati
19
19
 
20
20
  - canonical tagged values and RFC 8785 encoding
21
21
  - strict simple and qualified logical identifiers
22
+ - signed semantic client descriptors and strict HTTP/MCP client queries
22
23
  - closed semantic expression and query envelopes
23
24
  - portable input and output schemas
24
25
  - trusted query implementation artifacts
@@ -75,3 +76,19 @@ Self-hosted `@hypequery/serve` can continue to run application source directly.
75
76
  ## License
76
77
 
77
78
  Apache-2.0.
79
+
80
+ ## Semantic client contracts
81
+
82
+ RFC 0018 describes registered public endpoint capabilities for type generation
83
+ and HTTP-backed MCP. Use `decodeProtocolClientContract(text)` at an untrusted JSON
84
+ text boundary (bounded, rejects duplicate members), or
85
+ `validateProtocolClientContract(value)` for an already decoded signed object.
86
+ `prepareProtocolClientContract(unsigned)` computes the domain-separated schema
87
+ and view hashes and refuses preexisting hash fields.
88
+
89
+ `validateProtocolClientQuery(contract, endpointId, input, 'http' | 'mcp')` validates
90
+ and normalizes the strict client profile. Its result contains the registered
91
+ endpoint, selected measures, effective clamped limit and HTTP body. It maps MCP
92
+ grain to by and removes routing arguments. All returned data is a frozen snapshot.
93
+ These APIs do not authenticate, register a route, export application definitions
94
+ or execute queries. An endpoint hash is a compatibility identity, not permission.
@@ -1,5 +1,5 @@
1
- import { sha256 } from '@noble/hashes/sha2';
2
- import { bytesToHex } from '@noble/hashes/utils';
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import { bytesToHex } from '@noble/hashes/utils.js';
3
3
  import { serializeJcs } from '../values/jcs.js';
4
4
  import { validateProtocolDeploymentBundleManifest } from './validate.js';
5
5
  const textEncoder = new TextEncoder();
@@ -1,5 +1,5 @@
1
- import { hmac } from '@noble/hashes/hmac';
2
- import { sha256 } from '@noble/hashes/sha2';
1
+ import { hmac } from '@noble/hashes/hmac.js';
2
+ import { sha256 } from '@noble/hashes/sha2.js';
3
3
  import { validateProtocolDeploymentReleaseTarget } from '../releases/validate.js';
4
4
  import { PROTOCOL_CACHE_KEY_LIMITS, } from './types.js';
5
5
  const SCHEME = 'hq1';
@@ -1,6 +1,6 @@
1
- import { hmac } from '@noble/hashes/hmac';
2
- import { sha256 } from '@noble/hashes/sha2';
3
- import { bytesToHex } from '@noble/hashes/utils';
1
+ import { hmac } from '@noble/hashes/hmac.js';
2
+ import { sha256 } from '@noble/hashes/sha2.js';
3
+ import { bytesToHex } from '@noble/hashes/utils.js';
4
4
  import { validateProtocolSemanticQuery } from '../expressions/validate.js';
5
5
  import { serializeJcs } from '../values/jcs.js';
6
6
  const FINGERPRINT_DOMAIN = 'hypequery.tenant.fingerprint.v1';
@@ -0,0 +1,3 @@
1
+ /** Run count budgets on the detached JSON tree before descriptor validation. */
2
+ export declare function boundClientDescriptors(raw: Record<string, unknown>): void;
3
+ //# sourceMappingURL=bounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounds.d.ts","sourceRoot":"","sources":["../../src/client-contract/bounds.ts"],"names":[],"mappings":"AACA,gFAAgF;AAChF,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAgBzE"}
@@ -0,0 +1,24 @@
1
+ import { clientError } from './errors.js';
2
+ /** Run count budgets on the detached JSON tree before descriptor validation. */
3
+ export function boundClientDescriptors(raw) {
4
+ if (!Array.isArray(raw.endpoints))
5
+ return;
6
+ if (raw.endpoints.length > 256)
7
+ clientError('HQ_CLIENT_LIMIT', '/endpoints');
8
+ let descriptors = 0;
9
+ for (const [index, endpoint] of raw.endpoints.entries()) {
10
+ if (!endpoint || typeof endpoint !== 'object' || Array.isArray(endpoint))
11
+ continue;
12
+ for (const key of ['dimensions', 'measures', 'filters']) {
13
+ const value = endpoint[key];
14
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
15
+ const count = Object.keys(value).length;
16
+ if (count > 256)
17
+ clientError('HQ_CLIENT_LIMIT', `/endpoints/${index}/${key}`);
18
+ descriptors += count;
19
+ if (descriptors > 16384)
20
+ clientError('HQ_CLIENT_LIMIT', `/endpoints/${index}`);
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ export declare const CLIENT_SCHEMA_DOMAIN = "hypequery:client-endpoint-schema:v1\0";
2
+ export declare const CLIENT_CONTRACT_DOMAIN = "hypequery:client-contract:v1\0";
3
+ export declare function clientDigest(domain: string, value: unknown): string;
4
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/client-contract/codec.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,oBAAoB,0CAA0C,CAAC;AAC5E,eAAO,MAAM,sBAAsB,mCAAmC,CAAC;AACvE,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnE"}
@@ -0,0 +1,8 @@
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import { bytesToHex } from '@noble/hashes/utils.js';
3
+ import { clientCanonical } from './safe-data.js';
4
+ export const CLIENT_SCHEMA_DOMAIN = 'hypequery:client-endpoint-schema:v1\0';
5
+ export const CLIENT_CONTRACT_DOMAIN = 'hypequery:client-contract:v1\0';
6
+ export function clientDigest(domain, value) {
7
+ return bytesToHex(sha256(new TextEncoder().encode(domain + clientCanonical(value))));
8
+ }
@@ -0,0 +1,4 @@
1
+ import type { ProtocolClientContract } from './types.js';
2
+ /** Decode untrusted JSON text without accepting duplicate object members. */
3
+ export declare function decodeProtocolClientContract(input: string): ProtocolClientContract;
4
+ //# sourceMappingURL=decode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/client-contract/decode.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAiEzD,6EAA6E;AAC7E,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,sBAAsB,CAUlF"}
@@ -0,0 +1,100 @@
1
+ import { clientError, ProtocolClientContractError } from './errors.js';
2
+ import { CLIENT_MAX_BYTES } from './safe-data.js';
3
+ import { pointer } from './primitives.js';
4
+ import { validateProtocolClientContract } from './validate.js';
5
+ /** Bounded JSON parser retaining duplicate members until they can be refused. */
6
+ class ClientJsonReader {
7
+ input;
8
+ position = 0;
9
+ nodes = 0;
10
+ constructor(input) {
11
+ this.input = input;
12
+ }
13
+ read() {
14
+ const result = this.value(1, '');
15
+ this.whitespace();
16
+ if (this.position !== this.input.length)
17
+ clientError('HQ_CLIENT_TYPE');
18
+ return result;
19
+ }
20
+ whitespace() {
21
+ while (' \t\r\n'.includes(this.input[this.position] ?? '\0'))
22
+ this.position++;
23
+ }
24
+ string() {
25
+ const start = this.position++;
26
+ while (this.position < this.input.length) {
27
+ const character = this.input[this.position++];
28
+ if (character === '\\')
29
+ this.position++;
30
+ else if (character === '"')
31
+ return JSON.parse(this.input.slice(start, this.position));
32
+ }
33
+ return clientError('HQ_CLIENT_TYPE');
34
+ }
35
+ value(depth, path) {
36
+ if (depth > 16 || ++this.nodes > 100_000)
37
+ clientError('HQ_CLIENT_LIMIT');
38
+ this.whitespace();
39
+ const character = this.input[this.position];
40
+ if (character === '"')
41
+ return this.string();
42
+ if (character === '{' || character === '[') {
43
+ const object = character === '{';
44
+ const close = object ? '}' : ']';
45
+ const result = Object.create(null);
46
+ const array = [];
47
+ this.position++;
48
+ this.whitespace();
49
+ if (this.input[this.position] !== close) {
50
+ for (;;) {
51
+ this.whitespace();
52
+ let key = String(array.length);
53
+ if (object) {
54
+ if (this.input[this.position] !== '"')
55
+ clientError('HQ_CLIENT_TYPE');
56
+ key = this.string();
57
+ if (Object.hasOwn(result, key))
58
+ clientError('HQ_CLIENT_DUPLICATE');
59
+ this.whitespace();
60
+ if (this.input[this.position++] !== ':')
61
+ clientError('HQ_CLIENT_TYPE');
62
+ }
63
+ const child = this.value(depth + 1, pointer(path, key));
64
+ if (object)
65
+ result[key] = child;
66
+ else
67
+ array.push(child);
68
+ this.whitespace();
69
+ if (this.input[this.position] !== ',')
70
+ break;
71
+ this.position++;
72
+ }
73
+ }
74
+ if (this.input[this.position++] !== close)
75
+ clientError('HQ_CLIENT_TYPE');
76
+ return object ? result : array;
77
+ }
78
+ const match = /^(?:null|true|false|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)/.exec(this.input.slice(this.position));
79
+ if (!match)
80
+ clientError('HQ_CLIENT_TYPE');
81
+ this.position += match[0].length;
82
+ return JSON.parse(match[0]);
83
+ }
84
+ }
85
+ /** Decode untrusted JSON text without accepting duplicate object members. */
86
+ export function decodeProtocolClientContract(input) {
87
+ if (typeof input !== 'string')
88
+ clientError('HQ_CLIENT_TYPE');
89
+ if (input.length > CLIENT_MAX_BYTES || new TextEncoder().encode(input).length > CLIENT_MAX_BYTES) {
90
+ clientError('HQ_CLIENT_LIMIT');
91
+ }
92
+ try {
93
+ return validateProtocolClientContract(new ClientJsonReader(input).read());
94
+ }
95
+ catch (error) {
96
+ if (error instanceof ProtocolClientContractError)
97
+ throw error;
98
+ return clientError('HQ_CLIENT_TYPE');
99
+ }
100
+ }
@@ -0,0 +1,21 @@
1
+ export declare const CLIENT_ERROR_MESSAGES: Readonly<{
2
+ HQ_CLIENT_TYPE: "Expected plain JSON data.";
3
+ HQ_CLIENT_LIMIT: "Client contract exceeds validation limits.";
4
+ HQ_CLIENT_VERSION: "Unsupported client contract version.";
5
+ HQ_CLIENT_UNKNOWN_FIELD: "Unknown client contract field.";
6
+ HQ_CLIENT_SHAPE: "Invalid client contract shape.";
7
+ HQ_CLIENT_IDENTIFIER: "Invalid client contract identifier.";
8
+ HQ_CLIENT_DUPLICATE: "Duplicate client contract entry.";
9
+ HQ_CLIENT_PATH: "Invalid client endpoint path.";
10
+ HQ_CLIENT_REFERENCE: "Inconsistent client contract reference.";
11
+ HQ_CLIENT_IDENTITY: "Client contract identity does not match.";
12
+ HQ_CLIENT_QUERY: "Invalid client query.";
13
+ }>;
14
+ export type ProtocolClientContractErrorCode = keyof typeof CLIENT_ERROR_MESSAGES;
15
+ export declare class ProtocolClientContractError extends TypeError {
16
+ readonly code: ProtocolClientContractErrorCode;
17
+ readonly path: string;
18
+ constructor(code: ProtocolClientContractErrorCode, path?: string);
19
+ }
20
+ export declare function clientError(code: ProtocolClientContractErrorCode, path?: string): never;
21
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/client-contract/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB;;;;;;;;;;;;EAYhC,CAAC;AACH,MAAM,MAAM,+BAA+B,GAAG,MAAM,OAAO,qBAAqB,CAAC;AACjF,qBAAa,2BAA4B,SAAQ,SAAS;IACxD,QAAQ,CAAC,IAAI,EAAE,+BAA+B,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,IAAI,EAAE,+BAA+B,EAAE,IAAI,SAAK;CAM7D;AACD,wBAAgB,WAAW,CAAC,IAAI,EAAE,+BAA+B,EAAE,IAAI,SAAK,GAAG,KAAK,CAEnF"}
@@ -0,0 +1,26 @@
1
+ export const CLIENT_ERROR_MESSAGES = Object.freeze({
2
+ HQ_CLIENT_TYPE: 'Expected plain JSON data.',
3
+ HQ_CLIENT_LIMIT: 'Client contract exceeds validation limits.',
4
+ HQ_CLIENT_VERSION: 'Unsupported client contract version.',
5
+ HQ_CLIENT_UNKNOWN_FIELD: 'Unknown client contract field.',
6
+ HQ_CLIENT_SHAPE: 'Invalid client contract shape.',
7
+ HQ_CLIENT_IDENTIFIER: 'Invalid client contract identifier.',
8
+ HQ_CLIENT_DUPLICATE: 'Duplicate client contract entry.',
9
+ HQ_CLIENT_PATH: 'Invalid client endpoint path.',
10
+ HQ_CLIENT_REFERENCE: 'Inconsistent client contract reference.',
11
+ HQ_CLIENT_IDENTITY: 'Client contract identity does not match.',
12
+ HQ_CLIENT_QUERY: 'Invalid client query.',
13
+ });
14
+ export class ProtocolClientContractError extends TypeError {
15
+ code;
16
+ path;
17
+ constructor(code, path = '') {
18
+ super(CLIENT_ERROR_MESSAGES[code]);
19
+ this.name = 'ProtocolClientContractError';
20
+ this.code = code;
21
+ this.path = path;
22
+ }
23
+ }
24
+ export function clientError(code, path = '') {
25
+ throw new ProtocolClientContractError(code, path);
26
+ }
@@ -0,0 +1,8 @@
1
+ export { validateProtocolClientContract, prepareProtocolClientContract } from './validate.js';
2
+ export { ProtocolClientContractError, CLIENT_ERROR_MESSAGES } from './errors.js';
3
+ export type { ProtocolClientContractErrorCode } from './errors.js';
4
+ export type { ClientDimension, ClientMeasure, ClientFilter, ClientTime, ClientQueryLimits, ClientDatasetEndpoint, ClientMetricEndpoint, ProtocolClientEndpoint, ProtocolClientContract, PreparedProtocolClientContract, ClientFieldType, ClientWireType, } from './types.js';
5
+ export { validateProtocolClientQuery } from './query.js';
6
+ export type { ClientQueryPlan } from './query.js';
7
+ export { decodeProtocolClientContract } from './decode.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client-contract/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAC;AAC9F,OAAO,EAAE,2BAA2B,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACjF,YAAY,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EACV,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAC3E,qBAAqB,EAAE,oBAAoB,EAAE,sBAAsB,EACnE,sBAAsB,EAAE,8BAA8B,EAAE,eAAe,EAAE,cAAc,GACxF,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { validateProtocolClientContract, prepareProtocolClientContract } from './validate.js';
2
+ export { ProtocolClientContractError, CLIENT_ERROR_MESSAGES } from './errors.js';
3
+ export { validateProtocolClientQuery } from './query.js';
4
+ export { decodeProtocolClientContract } from './decode.js';
@@ -0,0 +1,8 @@
1
+ import type { ClientDimension, ClientFilter, ClientMeasure, ClientQueryLimits, ClientTime } from './types.js';
2
+ export declare function dimension(input: unknown, path: string): ClientDimension;
3
+ export declare function measure(input: unknown, path: string): ClientMeasure;
4
+ export declare function filter(input: unknown, path: string): ClientFilter;
5
+ export declare function fields<T>(input: unknown, path: string, validate: (v: unknown, p: string) => T): Record<string, T>;
6
+ export declare function time(input: unknown, path: string): ClientTime | null;
7
+ export declare function limits(input: unknown, path: string): ClientQueryLimits;
8
+ //# sourceMappingURL=nodes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../../src/client-contract/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAK9G,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CAYvE;AACD,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAKnE;AACD,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAQjE;AACD,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CASjH;AACD,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CASpE;AACD,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAUtE"}
@@ -0,0 +1,68 @@
1
+ import { clientError } from './errors.js';
2
+ import { bool, choice, CLIENT_FIELD_TYPES, CLIENT_GRAINS, CLIENT_OPERATORS, integer, name, names, pointer, record, shape, timezone } from './primitives.js';
3
+ export function dimension(input, path) {
4
+ const value = shape(input, ['type', 'wireType', 'nullable', 'groupable', 'orderable'], path);
5
+ const type = choice(value.type, CLIENT_FIELD_TYPES, pointer(path, 'type'));
6
+ const wires = type === 'number' ? ['number', 'string', 'number-or-string']
7
+ : type === 'boolean' ? ['boolean'] : ['string'];
8
+ const wireType = choice(value.wireType, wires, pointer(path, 'wireType'));
9
+ if (typeof value.nullable !== 'boolean' && value.nullable !== 'unknown') {
10
+ clientError('HQ_CLIENT_SHAPE', pointer(path, 'nullable'));
11
+ }
12
+ return { type, wireType, nullable: value.nullable,
13
+ groupable: bool(value.groupable, pointer(path, 'groupable')),
14
+ orderable: bool(value.orderable, pointer(path, 'orderable')) };
15
+ }
16
+ export function measure(input, path) {
17
+ const value = shape(input, ['wireType', 'nullable', 'having'], path);
18
+ if (value.wireType !== 'string')
19
+ clientError('HQ_CLIENT_SHAPE', pointer(path, 'wireType'));
20
+ if (value.nullable !== true)
21
+ clientError('HQ_CLIENT_SHAPE', pointer(path, 'nullable'));
22
+ return { wireType: 'string', nullable: true, having: bool(value.having, pointer(path, 'having')) };
23
+ }
24
+ export function filter(input, path) {
25
+ const value = shape(input, ['type', 'operators'], path);
26
+ const type = choice(value.type, CLIENT_FIELD_TYPES, pointer(path, 'type'));
27
+ const allowed = type === 'boolean' ? ['eq', 'neq', 'in', 'notIn']
28
+ : type === 'number' ? CLIENT_OPERATORS.filter(item => item !== 'like') : CLIENT_OPERATORS;
29
+ const operators = names(value.operators, pointer(path, 'operators'), allowed);
30
+ if (operators.length === 0)
31
+ clientError('HQ_CLIENT_SHAPE', pointer(path, 'operators'));
32
+ return { type, operators: operators.sort() };
33
+ }
34
+ export function fields(input, path, validate) {
35
+ const value = record(input, path);
36
+ if (Object.keys(value).length > 256)
37
+ clientError('HQ_CLIENT_LIMIT', path);
38
+ const result = Object.create(null);
39
+ for (const key of Object.keys(value).sort()) {
40
+ name(key, path, true);
41
+ result[key] = validate(value[key], pointer(path, key));
42
+ }
43
+ return result;
44
+ }
45
+ export function time(input, path) {
46
+ if (input === null)
47
+ return null;
48
+ const value = shape(input, ['grains', 'defaultTimezone', 'timezone', 'periodFormat'], path);
49
+ const grains = names(value.grains, pointer(path, 'grains'), CLIENT_GRAINS);
50
+ if (grains.length === 0)
51
+ clientError('HQ_CLIENT_SHAPE', pointer(path, 'grains'));
52
+ const defaultTimezone = timezone(value.defaultTimezone, pointer(path, 'defaultTimezone'));
53
+ const enabled = bool(value.timezone, pointer(path, 'timezone'));
54
+ if (value.periodFormat !== 'calendar-date-or-local-datetime-v1')
55
+ clientError('HQ_CLIENT_SHAPE', pointer(path, 'periodFormat'));
56
+ return { grains: grains.sort(), defaultTimezone, timezone: enabled, periodFormat: value.periodFormat };
57
+ }
58
+ export function limits(input, path) {
59
+ const bounds = { defaultLimit: [1, 100000], maxLimit: [1, 100000], maxOffset: [0, 1000000],
60
+ maxDimensions: [0, 100], maxMeasures: [0, 100], maxPredicates: [0, 100],
61
+ maxOrderBy: [0, 100], maxFilterValues: [1, 10000] };
62
+ const value = shape(input, Object.keys(bounds), path);
63
+ const result = Object.create(null);
64
+ for (const [key, [low, high]] of Object.entries(bounds)) {
65
+ result[key] = integer(value[key], low, high, pointer(path, key));
66
+ }
67
+ return result;
68
+ }
@@ -0,0 +1,15 @@
1
+ export type ClientRecord = Record<string, unknown>;
2
+ export declare const CLIENT_FIELD_TYPES: readonly ["string", "number", "boolean", "timestamp"];
3
+ export declare const CLIENT_GRAINS: readonly ["minute", "hour", "day", "week", "month", "quarter", "year"];
4
+ export declare const CLIENT_OPERATORS: string[];
5
+ export declare function pointer(path: string, key: string | number): string;
6
+ export declare function record(input: unknown, path: string): ClientRecord;
7
+ export declare function shape(input: unknown, keys: readonly string[], path: string): ClientRecord;
8
+ export declare function choice<T extends string>(input: unknown, choices: readonly T[], path: string): T;
9
+ export declare function bool(input: unknown, path: string): boolean;
10
+ export declare function integer(input: unknown, low: number, high: number, path: string): number;
11
+ export declare function name(input: unknown, path: string, qualified?: boolean): string;
12
+ export declare function names(input: unknown, path: string, allowed?: readonly string[]): string[];
13
+ export declare function route(input: unknown, path: string): string;
14
+ export declare function timezone(input: unknown, path: string): string;
15
+ //# sourceMappingURL=primitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/client-contract/primitives.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,eAAO,MAAM,kBAAkB,uDAAwD,CAAC;AACxF,eAAO,MAAM,aAAa,wEAAyE,CAAC;AACpG,eAAO,MAAM,gBAAgB,UAA4E,CAAC;AAC1G,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAElE;AACD,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAGjE;AACD,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAKzF;AACD,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAG/F;AACD,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAG1D;AACD,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKvF;AACD,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,MAAM,CAO5E;AACD,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAOzF;AACD,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AACD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7D"}
@@ -0,0 +1,74 @@
1
+ import { CLIENT_TIMEZONE_NAMES } from './timezone-names.js';
2
+ import { parseProtocolIdentifier } from '../identifiers/identifiers.js';
3
+ import { clientError } from './errors.js';
4
+ export const CLIENT_FIELD_TYPES = ['string', 'number', 'boolean', 'timestamp'];
5
+ export const CLIENT_GRAINS = ['minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'];
6
+ export const CLIENT_OPERATORS = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'between', 'like'];
7
+ export function pointer(path, key) {
8
+ return path + '/' + String(key).replaceAll('~', '~0').replaceAll('/', '~1');
9
+ }
10
+ export function record(input, path) {
11
+ if (!input || typeof input !== 'object' || Array.isArray(input))
12
+ clientError('HQ_CLIENT_SHAPE', path);
13
+ return input;
14
+ }
15
+ export function shape(input, keys, path) {
16
+ const value = record(input, path);
17
+ if (Object.keys(value).some(key => !keys.includes(key)))
18
+ clientError('HQ_CLIENT_UNKNOWN_FIELD', path);
19
+ if (keys.some(key => !Object.hasOwn(value, key)))
20
+ clientError('HQ_CLIENT_SHAPE', path);
21
+ return value;
22
+ }
23
+ export function choice(input, choices, path) {
24
+ if (typeof input !== 'string' || !choices.includes(input))
25
+ clientError('HQ_CLIENT_SHAPE', path);
26
+ return input;
27
+ }
28
+ export function bool(input, path) {
29
+ if (typeof input !== 'boolean')
30
+ clientError('HQ_CLIENT_SHAPE', path);
31
+ return input;
32
+ }
33
+ export function integer(input, low, high, path) {
34
+ if (!Number.isSafeInteger(input) || input < low || input > high) {
35
+ clientError('HQ_CLIENT_SHAPE', path);
36
+ }
37
+ return input;
38
+ }
39
+ export function name(input, path, qualified = false) {
40
+ if (typeof input !== 'string')
41
+ clientError('HQ_CLIENT_IDENTIFIER', path);
42
+ const parts = input.split('.');
43
+ if (parts.length > (qualified ? 2 : 1))
44
+ clientError('HQ_CLIENT_IDENTIFIER', path);
45
+ try {
46
+ for (const part of parts)
47
+ parseProtocolIdentifier(part);
48
+ }
49
+ catch {
50
+ clientError('HQ_CLIENT_IDENTIFIER', path);
51
+ }
52
+ return input;
53
+ }
54
+ export function names(input, path, allowed) {
55
+ if (!Array.isArray(input))
56
+ clientError('HQ_CLIENT_SHAPE', path);
57
+ const result = input.map((item, index) => allowed
58
+ ? choice(item, allowed, pointer(path, index))
59
+ : name(item, pointer(path, index), true));
60
+ if (new Set(result).size !== result.length)
61
+ clientError('HQ_CLIENT_DUPLICATE', path);
62
+ return result;
63
+ }
64
+ export function route(input, path) {
65
+ if (typeof input !== 'string' || !/^\/(?:[A-Za-z0-9_~.-]+\/)*[A-Za-z0-9_~.-]+$/.test(input)
66
+ || input.split('/').some(part => part === '.' || part === '..'))
67
+ clientError('HQ_CLIENT_PATH', path);
68
+ return input;
69
+ }
70
+ export function timezone(input, path) {
71
+ if (typeof input !== 'string' || !CLIENT_TIMEZONE_NAMES.has(input))
72
+ clientError('HQ_CLIENT_SHAPE', path);
73
+ return input;
74
+ }
@@ -0,0 +1,10 @@
1
+ import type { ProtocolClientEndpoint } from './types.js';
2
+ export interface ClientQueryPlan {
3
+ readonly endpoint: ProtocolClientEndpoint;
4
+ readonly body: Readonly<Record<string, unknown>>;
5
+ readonly selectedMeasures: readonly string[];
6
+ readonly effectiveLimit: number;
7
+ }
8
+ /** Normalize HTTP or MCP input under a signed contract; routing never comes from a tool URL. */
9
+ export declare function validateProtocolClientQuery(contract: unknown, endpointId: string, input: unknown, transport?: 'http' | 'mcp'): ClientQueryPlan;
10
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/client-contract/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAKzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AA2ED,gGAAgG;AAChG,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,GAAE,MAAM,GAAG,KAAc,GACxF,eAAe,CAmBjB"}
@@ -0,0 +1,121 @@
1
+ import { clientError, ProtocolClientContractError } from './errors.js';
2
+ import { validateProtocolClientContract } from './validate.js';
3
+ import { record, shape, integer, timezone } from './primitives.js';
4
+ import { freezeClientData, snapshotClientData } from './safe-data.js';
5
+ function array(input, maximum) {
6
+ if (!Array.isArray(input) || input.length > maximum)
7
+ clientError('HQ_CLIENT_QUERY');
8
+ return input;
9
+ }
10
+ function strings(input, allowed, maximum) {
11
+ const values = array(input, maximum);
12
+ if (values.some(v => typeof v !== 'string' || !allowed.includes(v))
13
+ || new Set(values).size !== values.length)
14
+ clientError('HQ_CLIENT_QUERY');
15
+ return values;
16
+ }
17
+ function predicateValue(value, type, operator, maximum) {
18
+ const values = operator === 'between' || operator === 'in' || operator === 'notIn'
19
+ ? array(value, operator === 'between' ? 2 : maximum) : [value];
20
+ if (values.length === 0 || (operator === 'between' && values.length !== 2))
21
+ clientError('HQ_CLIENT_QUERY');
22
+ for (const item of values) {
23
+ if (type === 'number' ? typeof item !== 'number' || !Number.isFinite(item)
24
+ : type === 'boolean' ? typeof item !== 'boolean' : typeof item !== 'string')
25
+ clientError('HQ_CLIENT_QUERY');
26
+ }
27
+ }
28
+ function bodyFor(endpoint, source, transport) {
29
+ const keys = ['dimensions', 'filters', 'orderBy', 'limit', 'offset', 'includeMeta',
30
+ ...(endpoint.kind === 'dataset' ? ['measures', 'having'] : []),
31
+ ...(endpoint.time ? ['by', ...(endpoint.time.timezone ? ['timezone'] : [])] : [])];
32
+ if (Object.keys(source).some(key => !keys.includes(key)))
33
+ clientError('HQ_CLIENT_QUERY');
34
+ const caps = endpoint.limits;
35
+ const dimensions = strings(source.dimensions === undefined ? [] : source.dimensions, Object.keys(endpoint.dimensions).filter(key => endpoint.dimensions[key].groupable), caps.maxDimensions);
36
+ const selectedMeasures = endpoint.kind === 'metric' ? Object.keys(endpoint.measures)
37
+ : strings(source.measures === undefined ? endpoint.defaultMeasures : source.measures, Object.keys(endpoint.measures), caps.maxMeasures);
38
+ if (!dimensions.length && !selectedMeasures.length && source.by === undefined)
39
+ clientError('HQ_CLIENT_QUERY');
40
+ const filters = array(source.filters === undefined ? [] : source.filters, caps.maxPredicates);
41
+ for (const item of filters) {
42
+ const filter = shape(item, ['field', 'operator', 'value'], '');
43
+ if (typeof filter.field !== 'string' || !Object.hasOwn(endpoint.filters, filter.field))
44
+ clientError('HQ_CLIENT_QUERY');
45
+ const definition = endpoint.filters[filter.field];
46
+ if (typeof filter.operator !== 'string' || !definition.operators.includes(filter.operator))
47
+ clientError('HQ_CLIENT_QUERY');
48
+ predicateValue(filter.value, definition.type, filter.operator, caps.maxFilterValues);
49
+ }
50
+ const having = array(source.having === undefined ? [] : source.having, caps.maxPredicates - filters.length);
51
+ for (const item of having) {
52
+ const condition = shape(item, ['measure', 'operator', 'value'], '');
53
+ if (typeof condition.measure !== 'string' || !selectedMeasures.includes(condition.measure)
54
+ || !endpoint.measures[condition.measure].having
55
+ || typeof condition.operator !== 'string'
56
+ || !['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn'].includes(condition.operator))
57
+ clientError('HQ_CLIENT_QUERY');
58
+ predicateValue(condition.value, 'number', condition.operator, caps.maxFilterValues);
59
+ }
60
+ if (source.by !== undefined && (typeof source.by !== 'string' || !endpoint.time?.grains.includes(source.by)))
61
+ clientError('HQ_CLIENT_QUERY');
62
+ if (source.timezone !== undefined)
63
+ timezone(source.timezone, '/timezone');
64
+ const orderBy = array(source.orderBy === undefined ? [] : source.orderBy, caps.maxOrderBy);
65
+ const orderable = [...dimensions.filter(key => endpoint.dimensions[key].orderable),
66
+ ...selectedMeasures, ...(source.by === undefined ? [] : ['period'])];
67
+ const ordered = new Set();
68
+ for (const item of orderBy) {
69
+ const order = shape(item, ['field', 'direction'], '');
70
+ if (typeof order.field !== 'string' || !orderable.includes(order.field) || ordered.has(order.field)
71
+ || (order.direction !== 'asc' && order.direction !== 'desc'))
72
+ clientError('HQ_CLIENT_QUERY');
73
+ ordered.add(order.field);
74
+ }
75
+ // HTTP clamps a larger limit; an agent is refused one, so it never reads a
76
+ // silently shortened page as the whole answer.
77
+ const effectiveLimit = Math.min(source.limit === undefined ? caps.defaultLimit
78
+ : integer(source.limit, 1, transport === 'mcp' ? caps.maxLimit : Number.MAX_SAFE_INTEGER, '/limit'), caps.maxLimit);
79
+ const offset = source.offset === undefined ? 0 : integer(source.offset, 0, caps.maxOffset, '/offset');
80
+ if (source.includeMeta !== undefined && typeof source.includeMeta !== 'boolean')
81
+ clientError('HQ_CLIENT_QUERY');
82
+ if (source.includeMeta === true && !endpoint.metadata)
83
+ clientError('HQ_CLIENT_QUERY');
84
+ const body = { dimensions, filters, orderBy, limit: effectiveLimit, offset,
85
+ includeMeta: source.includeMeta ?? false,
86
+ ...(endpoint.kind === 'dataset' ? { measures: selectedMeasures, having } : {}),
87
+ ...(source.by === undefined ? {} : { by: source.by }),
88
+ ...(source.timezone === undefined ? {} : { timezone: source.timezone }) };
89
+ return freezeClientData({ endpoint, body, selectedMeasures, effectiveLimit });
90
+ }
91
+ /** Normalize HTTP or MCP input under a signed contract; routing never comes from a tool URL. */
92
+ export function validateProtocolClientQuery(contract, endpointId, input, transport = 'http') {
93
+ const validated = validateProtocolClientContract(contract);
94
+ try {
95
+ const endpoint = validated.endpoints.find(item => item.id === endpointId);
96
+ if (!endpoint)
97
+ clientError('HQ_CLIENT_QUERY');
98
+ const source = record(snapshotClientData(input), '');
99
+ if (transport === 'mcp') {
100
+ if (source.dataset !== endpoint.dataset || (endpoint.kind === 'metric'
101
+ ? source.metric !== endpoint.id : Object.hasOwn(source, 'metric')))
102
+ clientError('HQ_CLIENT_QUERY');
103
+ delete source.dataset;
104
+ delete source.metric;
105
+ if (Object.hasOwn(source, 'by'))
106
+ clientError('HQ_CLIENT_QUERY');
107
+ if (Object.hasOwn(source, 'grain')) {
108
+ source.by = source.grain;
109
+ delete source.grain;
110
+ }
111
+ }
112
+ else if (transport !== 'http')
113
+ clientError('HQ_CLIENT_QUERY');
114
+ return bodyFor(endpoint, source, transport);
115
+ }
116
+ catch (error) {
117
+ if (error instanceof ProtocolClientContractError)
118
+ clientError('HQ_CLIENT_QUERY', error.path);
119
+ throw error;
120
+ }
121
+ }
@@ -0,0 +1,6 @@
1
+ export declare const CLIENT_MAX_BYTES = 1048576;
2
+ /** Snapshot before validation; accessors/toJSON are never invoked. Reflection can trigger Proxy traps. */
3
+ export declare function snapshotClientData(input: unknown): unknown;
4
+ export declare function clientCanonical(value: unknown): string;
5
+ export declare function freezeClientData<T>(value: T): T;
6
+ //# sourceMappingURL=safe-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-data.d.ts","sourceRoot":"","sources":["../../src/client-contract/safe-data.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB,UAAY,CAAC;AAuE1C,0GAA0G;AAC1G,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM1D;AACD,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAItD;AACD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAM/C"}