@callcium/sdk 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/descriptor.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { readU16, readU24, writeBE16, writeBE32 } from "./bytes";
2
- import { DescriptorFormat as DF, PolicyFormat as PF, TypeCode } from "./constants";
1
+ import { readU16, readU24 } from "./bytes";
2
+ import { DescriptorFormat as DF, TypeCode } from "./constants";
3
3
  import { CallciumError } from "./errors";
4
- import { classifyTypeCode } from "./operators";
5
- import { Quantifier, isQuantifier } from "./path";
4
+ import { lookupTypeCode } from "./operators";
6
5
 
7
6
  ///////////////////////////////////////////////////////////////////////////
8
7
  // Public types
@@ -15,29 +14,6 @@ export type TypeInfo = {
15
14
  staticSize: number;
16
15
  };
17
16
 
18
- ///////////////////////////////////////////////////////////////////////////
19
- // Internal types
20
- ///////////////////////////////////////////////////////////////////////////
21
-
22
- /** A hop entry of a compiled chain: [delta, index, meta]. */
23
- type Hop = [number, number, number];
24
-
25
- /** State threaded through the compilation of a path into a hint block. */
26
- type HintWalk = {
27
- /** Hops of the chain currently being compiled. */
28
- chain: Hop[];
29
- /** Hops of the main chain, captured when a quantifier closes it. */
30
- mainHops: Hop[];
31
- /** Byte offset of the next node relative to the chain's current base. */
32
- delta: number;
33
- /** Frame prefix fields, captured when a quantifier closes the main chain. */
34
- frame: { arrayDelta: number; count: number; meta: number };
35
- /** Header kind the path resolves to. */
36
- kind: number;
37
- /** Set while the chain already ends inside the current node, which then takes no entry hop. */
38
- entered: boolean;
39
- };
40
-
41
17
  ///////////////////////////////////////////////////////////////////////////
42
18
  // Internal helpers
43
19
  ///////////////////////////////////////////////////////////////////////////
@@ -46,7 +22,7 @@ type HintWalk = {
46
22
  type Node = { typeCode: number; staticWords: number; nodeLength: number };
47
23
 
48
24
  /**
49
- * Read the node at `offset`. Elementary nodes occupy exactly one byte; composite nodes encode their static word count
25
+ * Read the node at `offset`. Elementary nodes occupy exactly one byte. Composite nodes encode their static word count
50
26
  * and length in the 24-bit meta field that follows the type code.
51
27
  * @throws {CallciumError} When the node starts past the descriptor, carries an undefined type code, or declares a
52
28
  * length below its own header or beyond the descriptor.
@@ -56,7 +32,7 @@ export function readNode(desc: Uint8Array, offset: number): Node {
56
32
  throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
57
33
  }
58
34
  const typeCode = desc[offset]!;
59
- const info = classifyTypeCode(typeCode);
35
+ const info = lookupTypeCode(typeCode);
60
36
  if (info.typeClass === "elementary") {
61
37
  return { typeCode, staticWords: info.isDynamic ? 0 : 1, nodeLength: DF.TYPECODE_SIZE };
62
38
  }
@@ -82,50 +58,25 @@ export function readNode(desc: Uint8Array, offset: number): Node {
82
58
 
83
59
  /**
84
60
  * Return the byte length of the node at `offset`.
85
- * @throws {CallciumError} When the descriptor holds no readable node at `offset`.
61
+ * @throws {CallciumError} When the descriptor contains no readable node at `offset`.
86
62
  */
87
63
  function nodeLength(desc: Uint8Array, offset: number): number {
88
64
  return readNode(desc, offset).nodeLength;
89
65
  }
90
66
 
91
- /**
92
- * Return the byte offset of the `fieldIndex`-th field inside a tuple node. Starts at the first field and skips
93
- * `fieldIndex` nodes using `nodeLength`.
94
- */
95
- function tupleFieldOffset(desc: Uint8Array, tupleOffset: number, fieldIndex: number): number {
96
- let cursor = tupleOffset + DF.TUPLE_HEADER_SIZE;
97
- for (let i = 0; i < fieldIndex; i++) {
98
- cursor += nodeLength(desc, cursor);
99
- }
100
- return cursor;
101
- }
102
-
103
67
  ///////////////////////////////////////////////////////////////////////////
104
68
  // Public interface
105
69
  ///////////////////////////////////////////////////////////////////////////
106
70
 
107
71
  /**
108
72
  * Return the declared top-level parameter count.
109
- * @throws {CallciumError} When the descriptor is too short to hold a header.
73
+ * @throws {CallciumError} When the descriptor is too short for a header.
110
74
  */
111
75
  function paramCount(desc: Uint8Array): number {
112
76
  if (desc.length < DF.HEADER_SIZE) {
113
77
  throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
114
78
  }
115
- return desc[1]!;
116
- }
117
-
118
- /** Return byte offset of the N-th top-level param (0-indexed). */
119
- function paramOffset(desc: Uint8Array, index: number): number {
120
- const count = paramCount(desc);
121
- if (index >= count) {
122
- throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})`);
123
- }
124
- let cursor = DF.HEADER_SIZE;
125
- for (let i = 0; i < index; i++) {
126
- cursor += nodeLength(desc, cursor);
127
- }
128
- return cursor;
79
+ return desc[DF.HEADER_PARAMCOUNT_OFFSET]!;
129
80
  }
130
81
 
131
82
  /**
@@ -133,91 +84,20 @@ function paramOffset(desc: Uint8Array, index: number): number {
133
84
  * @param desc - Raw descriptor bytes.
134
85
  * @param offset - Byte position of the node.
135
86
  * @returns typeCode, isDynamic, and staticSize in bytes.
136
- * @throws {CallciumError} When the descriptor holds no readable node at `offset`.
87
+ * @throws {CallciumError} When the descriptor contains no readable node at `offset`.
137
88
  */
138
89
  function inspect(desc: Uint8Array, offset: number): TypeInfo {
139
90
  const { typeCode, staticWords } = readNode(desc, offset);
140
91
  return { typeCode, isDynamic: staticWords === 0, staticSize: staticWords * 32 };
141
92
  }
142
93
 
143
- /**
144
- * Resolve the type at a calldata path (array of step indices).
145
- *
146
- * The first step selects a top-level param. Each subsequent step descends into the current node: for tuples it selects
147
- * a field, for arrays it advances to the element descriptor.
148
- *
149
- * @param desc - Raw descriptor bytes.
150
- * @param steps - Path steps, length >= 1.
151
- * @returns TypeInfo for the node at the resolved path.
152
- * @throws {CallciumError} On empty steps, an out-of-bounds param index, tuple field or static array index, or descent
153
- * into an elementary type.
154
- */
155
- function typeAt(desc: Uint8Array, steps: number[]): TypeInfo {
156
- return walkPath(desc, steps).typeInfo;
157
- }
158
-
159
- /**
160
- * Resolve the type at the path and the declared length of the static array a quantifier step descends into (zero when
161
- * the path holds no quantifier step over a static array).
162
- * @param desc - Raw descriptor bytes.
163
- * @param steps - Path steps, length >= 1.
164
- * @returns TypeInfo at the resolved path and the quantified static array length.
165
- * @throws {CallciumError} On empty steps, an out-of-bounds param index, tuple field or static array index, or descent
166
- * into an elementary type.
167
- */
168
- function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quantifiedStaticLength: number } {
169
- if (steps.length === 0) {
170
- throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
171
- }
172
-
173
- let cursor = paramOffset(desc, steps[0]!);
174
- let quantifiedStaticLength = 0;
175
-
176
- for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
177
- const typeCode = desc[cursor]!;
178
- const step = steps[stepIndex]!;
179
-
180
- if (typeCode === TypeCode.TUPLE) {
181
- const fields = tupleFieldCount(desc, cursor);
182
- if (step >= fields) {
183
- throw new CallciumError(
184
- "TUPLE_FIELD_OUT_OF_BOUNDS",
185
- `Tuple field index ${step} out of range (tuple has ${fields} fields)`,
186
- );
187
- }
188
- cursor = tupleFieldOffset(desc, cursor, step);
189
- } else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
190
- // Quantifier sentinels descend into the element type; only concrete indices are bounds-checked against the
191
- // declared array length.
192
- if (typeCode === TypeCode.STATIC_ARRAY) {
193
- if (isQuantifier(step)) {
194
- quantifiedStaticLength = staticArrayLength(desc, cursor);
195
- } else {
196
- const length = staticArrayLength(desc, cursor);
197
- if (step >= length) {
198
- throw new CallciumError(
199
- "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
200
- `Array index ${step} out of range (length=${length})`,
201
- );
202
- }
203
- }
204
- }
205
- cursor = arrayElementOffset(cursor);
206
- } else {
207
- throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}`);
208
- }
209
- }
210
-
211
- return { typeInfo: inspect(desc, cursor), quantifiedStaticLength };
212
- }
213
-
214
94
  /**
215
95
  * Return tuple field count at a tuple node offset.
216
96
  * @param desc - Raw descriptor bytes.
217
97
  * @param offset - Byte position of the tuple node.
218
98
  */
219
99
  function tupleFieldCount(desc: Uint8Array, offset: number): number {
220
- return readU16(desc, offset + 4);
100
+ return readU16(desc, offset + DF.TYPECODE_SIZE + DF.COMPOSITE_META_SIZE);
221
101
  }
222
102
 
223
103
  /**
@@ -240,188 +120,99 @@ function arrayElementOffset(offset: number): number {
240
120
  return offset + DF.ARRAY_HEADER_SIZE;
241
121
  }
242
122
 
243
- /** Build the error for a path step that does not resolve to a hop-chain target. */
244
- function uncompilablePath(stepIndex: number): CallciumError {
245
- return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target`);
246
- }
247
-
248
- /** Return the combined head slot span of the `count` nodes at `offset`, and the offset of the node past them. */
249
- function headSpan(desc: Uint8Array, offset: number, count: number): { span: number; nodeOffset: number } {
250
- let span = 0;
251
- let nodeOffset = offset;
252
- for (let i = 0; i < count; i++) {
253
- const node = inspect(desc, nodeOffset);
254
- // An indirected node occupies a single offset word.
255
- span += node.isDynamic ? 32 : node.staticSize;
256
- nodeOffset += nodeLength(desc, nodeOffset);
257
- }
258
- return { span, nodeOffset };
259
- }
260
-
261
- /** Return the meta word describing the array node at `arrayOffset`, and whether its elements are dynamic. */
262
- function arrayMeta(desc: Uint8Array, arrayOffset: number, code: number): { meta: number; elemIsDynamic: boolean } {
263
- const element = inspect(desc, arrayElementOffset(arrayOffset));
264
- // A dynamic element occupies one offset word within the element region.
265
- let meta = element.isDynamic ? 1 : element.staticSize >> 5;
266
- if (element.isDynamic) meta |= PF.HINT_META_ELEM_DYNAMIC;
267
- if (code === TypeCode.DYNAMIC_ARRAY) meta |= PF.HINT_META_DYNAMIC_ARRAY;
268
- return { meta, elemIsDynamic: element.isDynamic };
269
- }
270
-
271
- /** Append the hop entering an indirected node's payload and rebase the offset accumulator. */
272
- function enterNode(walk: HintWalk, needed: boolean): void {
273
- if (!needed) return;
274
- walk.chain.push([walk.delta, PF.HINT_NO_INDEX, 0]);
275
- walk.delta = 0;
276
- }
277
-
278
- /** Apply one path step to `walk` and return the descriptor offset the step reaches. */
279
- function walkStep(desc: Uint8Array, walk: HintWalk, descOffset: number, step: number, stepIndex: number): number {
280
- const node = inspect(desc, descOffset);
123
+ ///////////////////////////////////////////////////////////////////////////
124
+ // Metadata validity
125
+ ///////////////////////////////////////////////////////////////////////////
281
126
 
282
- // A quantifier reaches an array alone, every other code reading its value as an index.
283
- if (node.typeCode === TypeCode.TUPLE) {
284
- if (step >= tupleFieldCount(desc, descOffset)) throw uncompilablePath(stepIndex);
285
- enterNode(walk, node.isDynamic && !walk.entered);
127
+ /** Composite node metadata that disagrees with the node's children. */
128
+ export type MetaFault = "NODE_LENGTH" | "STATIC_WORDS";
286
129
 
287
- const field = headSpan(desc, descOffset + DF.TUPLE_HEADER_SIZE, step);
288
- walk.delta += field.span;
289
- walk.entered = false;
290
- return field.nodeOffset;
291
- }
130
+ /** A node's span and its type's static word count, or the first node whose metadata disagrees with them. */
131
+ type MetaWalk = { span: number; staticWords: number } | { fault: MetaFault; offset: number };
292
132
 
293
- if (node.typeCode !== TypeCode.STATIC_ARRAY && node.typeCode !== TypeCode.DYNAMIC_ARRAY) {
294
- throw uncompilablePath(stepIndex);
133
+ /** Walk the node at `offset` by the spans of its children. */
134
+ function walkMeta(desc: Uint8Array, offset: number): MetaWalk {
135
+ const node = readNode(desc, offset);
136
+ if (lookupTypeCode(node.typeCode).typeClass === "elementary") {
137
+ return { span: DF.TYPECODE_SIZE, staticWords: node.staticWords };
295
138
  }
296
- if (node.typeCode === TypeCode.STATIC_ARRAY && !isQuantifier(step) && step >= staticArrayLength(desc, descOffset)) {
297
- throw uncompilablePath(stepIndex);
298
- }
299
- const { meta, elemIsDynamic } = arrayMeta(desc, descOffset, node.typeCode);
300
-
301
- if (isQuantifier(step)) {
302
- if (walk.kind !== PF.HINT_KIND_NONE) throw uncompilablePath(stepIndex);
303
- enterNode(walk, node.isDynamic && !walk.entered);
304
139
 
305
- const count = node.typeCode === TypeCode.DYNAMIC_ARRAY ? 0 : staticArrayLength(desc, descOffset);
306
- walk.frame = { arrayDelta: walk.delta, count, meta };
307
- walk.kind = step === Quantifier.ALL ? PF.HINT_KIND_ALL : PF.HINT_KIND_ANY;
308
-
309
- walk.mainHops = walk.chain;
310
- walk.chain = [];
311
- walk.delta = 0;
312
- } else if (node.isDynamic) {
313
- // An indirected array holds its elements behind an offset word.
314
- enterNode(walk, !walk.entered);
315
- walk.chain.push([0, step, meta]);
316
- walk.delta = 0;
140
+ let span: number;
141
+ let staticWords = 0;
142
+ if (node.typeCode === TypeCode.TUPLE) {
143
+ span = DF.TUPLE_HEADER_SIZE;
144
+ let hasDynamicField = false;
145
+ const fieldCount = tupleFieldCount(desc, offset);
146
+ for (let i = 0; i < fieldCount; i++) {
147
+ const field = walkMeta(desc, offset + span);
148
+ if ("fault" in field) return field;
149
+ span += field.span;
150
+ staticWords += field.staticWords;
151
+ if (field.staticWords === 0) hasDynamicField = true;
152
+ }
153
+ if (hasDynamicField) staticWords = 0;
317
154
  } else {
318
- // A static array of static elements is inline, so the index folds into the accumulator.
319
- walk.delta += step * (meta & PF.HINT_META_STRIDE_MASK) * 32;
320
- }
321
-
322
- walk.entered = elemIsDynamic;
323
- return arrayElementOffset(descOffset);
324
- }
325
-
326
- /** Serialize hop entries at `offset` and return the offset past them. */
327
- function writeHops(out: Uint8Array, offset: number, hops: Hop[]): number {
328
- for (const [delta, index, meta] of hops) {
329
- writeBE32(out, offset, delta);
330
- writeBE16(out, offset + PF.HINT_HOP_INDEX_OFFSET, index);
331
- writeBE16(out, offset + PF.HINT_HOP_META_OFFSET, meta);
332
- offset += PF.HINT_HOP_SIZE;
155
+ const element = walkMeta(desc, arrayElementOffset(offset));
156
+ if ("fault" in element) return element;
157
+ span = DF.ARRAY_HEADER_SIZE + element.span;
158
+ if (node.typeCode === TypeCode.STATIC_ARRAY) {
159
+ staticWords = element.staticWords * readU16(desc, offset + span);
160
+ span += DF.ARRAY_LENGTH_SIZE;
161
+ }
333
162
  }
334
- return offset;
163
+ if (span !== node.nodeLength) return { fault: "NODE_LENGTH", offset };
164
+ if (staticWords !== node.staticWords) return { fault: "STATIC_WORDS", offset };
165
+ return { span, staticWords };
335
166
  }
336
167
 
337
- /** Serialize a completed walk and its target block into the wire hint block. */
338
- function encodeHint(walk: HintWalk, targetMeta: number, typeCode: number, depth: number): Uint8Array {
339
- let suffixHops: Hop[] = [];
340
- if (walk.kind === PF.HINT_KIND_NONE) walk.mainHops = walk.chain;
341
- else suffixHops = walk.chain;
342
-
343
- const mainHopCount = walk.mainHops.length;
344
- const suffixHopCount = suffixHops.length;
345
- if (mainHopCount > PF.HINT_HOP_COUNT_MASK || suffixHopCount > PF.HINT_HOP_COUNT_MASK) {
346
- throw uncompilablePath(depth);
347
- }
348
-
349
- const quantified = walk.kind !== PF.HINT_KIND_NONE;
350
- const frameSize = quantified
351
- ? PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE + suffixHopCount * PF.HINT_HOP_SIZE
352
- : 0;
353
- const out = new Uint8Array(PF.HINT_HEADER_SIZE + mainHopCount * PF.HINT_HOP_SIZE + frameSize + PF.HINT_TARGET_SIZE);
354
-
355
- let offset = 0;
356
- out[offset] = (walk.kind << PF.HINT_KIND_SHIFT) | mainHopCount;
357
- offset = writeHops(out, offset + PF.HINT_HEADER_SIZE, walk.mainHops);
358
-
359
- if (quantified) {
360
- writeBE32(out, offset, walk.frame.arrayDelta);
361
- writeBE16(out, offset + PF.HINT_FRAME_COUNT_OFFSET, walk.frame.count);
362
- writeBE16(out, offset + PF.HINT_FRAME_META_OFFSET, walk.frame.meta);
363
- out[offset + PF.HINT_FRAME_PREFIX_SIZE] = suffixHopCount;
364
- offset = writeHops(out, offset + PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE, suffixHops);
168
+ /**
169
+ * Find the first composite node whose metadata disagrees with its children (DV-1, DV-2). A node's children are checked
170
+ * before the node.
171
+ * @param desc - A well-formed descriptor.
172
+ * @returns The disagreement and the node's offset, or `undefined` when every node agrees.
173
+ */
174
+ function tryValidateMeta(desc: Uint8Array): { fault: MetaFault; offset: number } | undefined {
175
+ let cursor = DF.HEADER_SIZE;
176
+ const count = paramCount(desc);
177
+ for (let i = 0; i < count; i++) {
178
+ const walk = walkMeta(desc, cursor);
179
+ if ("fault" in walk) return walk;
180
+ cursor += walk.span;
365
181
  }
366
-
367
- writeBE32(out, offset, walk.delta);
368
- writeBE16(out, offset + PF.HINT_TARGET_META_OFFSET, targetMeta);
369
- out[offset + PF.HINT_TARGET_TYPECODE_OFFSET] = typeCode;
370
- return out;
182
+ return undefined;
371
183
  }
372
184
 
373
185
  /**
374
- * Compile a calldata rule path into its wire hint block.
375
- *
376
- * @param desc - Raw descriptor bytes.
377
- * @param steps - Path steps, length >= 1.
378
- * @returns The hint block bytes.
379
- * @throws {CallciumError} When a step leaves the structure the descriptor declares, quantifies over a non-array node,
380
- * or repeats a quantifier.
186
+ * Validate every composite node's metadata against its children (DV-1, DV-2).
187
+ * @param desc - A well-formed descriptor.
188
+ * @throws {CallciumError} With `NODE_LENGTH_MISMATCH` or `STATIC_WORDS_MISMATCH` at the first node at fault.
381
189
  */
382
- function compileHint(desc: Uint8Array, steps: number[]): Uint8Array {
383
- if (steps.length === 0) {
384
- throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
190
+ function validateMeta(desc: Uint8Array): void {
191
+ const found = tryValidateMeta(desc);
192
+ if (found?.fault === "NODE_LENGTH") {
193
+ throw new CallciumError(
194
+ "NODE_LENGTH_MISMATCH",
195
+ "Composite node length differs from the span of its children",
196
+ found.offset,
197
+ );
385
198
  }
386
-
387
- const argIndex = steps[0]!;
388
- if (argIndex >= paramCount(desc)) throw uncompilablePath(0);
389
-
390
- // The argument's head slot sits past the slots of every preceding parameter.
391
- const arg = headSpan(desc, DF.HEADER_SIZE, argIndex);
392
- const walk: HintWalk = {
393
- chain: [],
394
- mainHops: [],
395
- delta: arg.span,
396
- frame: { arrayDelta: 0, count: 0, meta: 0 },
397
- kind: PF.HINT_KIND_NONE,
398
- entered: false,
399
- };
400
-
401
- let descOffset = arg.nodeOffset;
402
- for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
403
- descOffset = walkStep(desc, walk, descOffset, steps[stepIndex]!, stepIndex);
199
+ if (found?.fault === "STATIC_WORDS") {
200
+ throw new CallciumError(
201
+ "STATIC_WORDS_MISMATCH",
202
+ "Composite node static word count differs from its type's head size",
203
+ found.offset,
204
+ );
404
205
  }
405
-
406
- const target = inspect(desc, descOffset);
407
- enterNode(walk, target.isDynamic && !walk.entered);
408
-
409
- const targetMeta = target.typeCode === TypeCode.DYNAMIC_ARRAY ? arrayMeta(desc, descOffset, target.typeCode).meta : 0;
410
-
411
- return encodeHint(walk, targetMeta, target.typeCode, steps.length);
412
206
  }
413
207
 
414
208
  /** Inspect and navigate raw descriptor bytes. */
415
209
  export const Descriptor = {
416
210
  paramCount,
417
- paramOffset,
418
211
  inspect,
419
- typeAt,
420
- tupleFieldOffset,
421
212
  tupleFieldCount,
422
213
  staticArrayLength,
423
214
  nodeLength,
424
215
  arrayElementOffset,
425
- walkPath,
426
- compileHint,
216
+ validateMeta,
217
+ tryValidateMeta,
427
218
  };
package/src/errors.ts CHANGED
@@ -1,26 +1,27 @@
1
+ import type { Issue, Violation } from "./types";
2
+
1
3
  /**
2
4
  * Machine-readable error code for structural decoding and validation failures.
3
5
  *
4
6
  * Each code names one invariant. Where Solidity detects the same invariant, the code is its error name transliterated
5
7
  * from `PascalCase` to `SCREAMING_SNAKE_CASE`. The codes under "Inputs and validation" have no counterpart: they name
6
- * input a Solidity signature cannot express. `OUT_OF_PHYSICAL_BOUNDS` names an invariant Solidity carries twice, as the
7
- * error `PolicyBuilder.OutOfPhysicalBounds` and as an issue code, so the name is shared with `IssueCode`.
8
+ * input a Solidity signature cannot express.
8
9
  */
9
10
  export type CallciumErrorCode =
10
11
  // Policy and descriptor framing.
11
12
  | "UNSUPPORTED_VERSION"
13
+ | "UNSUPPORTED_MINOR"
14
+ | "MINOR_MISMATCH"
12
15
  | "MALFORMED_HEADER"
13
16
  | "UNEXPECTED_END"
14
17
  | "TRAILING_BYTES"
15
18
  // Groups and rules.
16
19
  | "EMPTY_POLICY"
17
20
  | "EMPTY_GROUP"
18
- | "GROUP_TOO_SMALL"
19
21
  | "GROUP_SIZE_MISMATCH"
20
22
  | "GROUP_OVERFLOW"
21
23
  | "RULE_TOO_SMALL"
22
24
  | "RULE_SIZE_MISMATCH"
23
- | "RULE_FIELD_OUT_OF_BOUNDS"
24
25
  | "RULE_OVERFLOW"
25
26
  // Paths.
26
27
  | "EMPTY_PATH"
@@ -30,22 +31,25 @@ export type CallciumErrorCode =
30
31
  | "TUPLE_FIELD_OUT_OF_BOUNDS"
31
32
  | "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS"
32
33
  | "NOT_COMPOSITE"
33
- | "UNCOMPILABLE_PATH"
34
34
  | "QUANTIFIER_ON_NON_ARRAY"
35
- | "NESTED_QUANTIFIER"
35
+ | "UNKNOWN_PATH_STEP"
36
+ // Loops.
37
+ | "UNKNOWN_QUANTIFIER"
38
+ | "INVALID_NESTED_LOOP"
36
39
  // Scope and context.
37
- | "INVALID_SCOPE"
40
+ | "UNKNOWN_SCOPE"
38
41
  | "INVALID_CONTEXT_PATH"
39
42
  | "UNKNOWN_CONTEXT_PROPERTY"
40
43
  // Operators and hints.
41
44
  | "UNKNOWN_OPERATOR"
42
45
  | "INVALID_OPERATOR_BYTES"
43
46
  | "OPERATOR_TARGET_MISMATCH"
47
+ | "INVALID_CONTEXT_OPERAND"
48
+ | "DATA_LENGTH_MISMATCH"
44
49
  | "UNSORTED_IN_SET"
45
50
  | "MALFORMED_HINT"
46
51
  // Constraint building.
47
52
  | "NO_CONSTRAINT_OPERATORS"
48
- | "DUPLICATE_PATH_IN_GROUP"
49
53
  | "EMPTY_SET"
50
54
  | "SET_TOO_LARGE"
51
55
  | "INVALID_RANGE"
@@ -61,6 +65,12 @@ export type CallciumErrorCode =
61
65
  | "STATIC_WORDS_TOO_LARGE"
62
66
  | "PARAM_COUNT_MISMATCH"
63
67
  | "TOO_MANY_PARAMS"
68
+ | "NODE_LENGTH_MISMATCH"
69
+ | "STATIC_WORDS_MISMATCH"
70
+ // Type codes.
71
+ | "INVALID_UINT_BITS"
72
+ | "INVALID_INT_BITS"
73
+ | "INVALID_BYTES_LENGTH"
64
74
  // Type strings and signatures.
65
75
  | "MALFORMED_TYPE_STRING"
66
76
  | "UNKNOWN_TYPE"
@@ -75,12 +85,13 @@ export type CallciumErrorCode =
75
85
  // Strict build.
76
86
  | "VALIDATION_ERROR"
77
87
  // Inputs and validation.
78
- | "UNKNOWN_QUANTIFIER"
79
88
  | "INVALID_HEX"
80
89
  | "MALFORMED_SELECTOR"
81
90
  | "PATH_STEP_OVERFLOW"
82
91
  | "OPERAND_OVERFLOW"
83
- | "CONTEXT_VALUE_OVERFLOW";
92
+ | "CONTEXT_VALUE_OVERFLOW"
93
+ | "MALFORMED_CONTEXT_VALUE"
94
+ | "MALFORMED_OPERAND";
84
95
 
85
96
  /**
86
97
  * Thrown on malformed or inadmissible SDK input. `offset`, when set, is the error's byte position in the source blob.
@@ -95,7 +106,7 @@ export class CallciumError extends Error {
95
106
  super(`${prefix}${message}`);
96
107
  this.name = "CallciumError";
97
108
  this.code = code;
98
- // The property is declared optional, so it stays absent rather than present and undefined.
109
+ // `exactOptionalPropertyTypes` admits no `undefined` in the optional property.
99
110
  if (offset !== undefined) this.offset = offset;
100
111
  }
101
112
  }
@@ -104,12 +115,12 @@ export class CallciumError extends Error {
104
115
  * Thrown by `PolicyBuilder.build` when validation reports any issue.
105
116
  *
106
117
  * Carries every issue the validator found. The `Error.message` is the first issue's message — a diagnostic summary, not
107
- * a presentation contract. Consumers rendering issues to users should iterate `issues` directly.
118
+ * a presentation contract. To render issues to users, iterate `issues` directly.
108
119
  */
109
120
  export class ValidationError extends CallciumError {
110
- public readonly issues: import("./types").Issue[];
121
+ public readonly issues: Issue[];
111
122
 
112
- constructor(issues: import("./types").Issue[]) {
123
+ constructor(issues: Issue[]) {
113
124
  super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
114
125
  this.name = "ValidationError";
115
126
  this.issues = issues;
@@ -121,13 +132,13 @@ export class ValidationError extends CallciumError {
121
132
  * signals static issues in the policy itself.
122
133
  *
123
134
  * Carries the full list of structured violations (one per failed group). The `Error.message` is a minimal non-lossy
124
- * diagnostic summary built from the first violation's structured fields — not a presentation contract. Consumers
125
- * rendering violations to users should iterate `violations` directly.
135
+ * diagnostic summary built from the first violation's structured fields — not a presentation contract. To render
136
+ * violations to users, iterate `violations` directly.
126
137
  */
127
138
  export class PolicyViolationError extends Error {
128
- public readonly violations: import("./types").Violation[];
139
+ public readonly violations: Violation[];
129
140
 
130
- constructor(violations: import("./types").Violation[]) {
141
+ constructor(violations: Violation[]) {
131
142
  super(formatDiagnostic(violations[0]));
132
143
  this.name = "PolicyViolationError";
133
144
  this.violations = violations;
@@ -135,7 +146,7 @@ export class PolicyViolationError extends Error {
135
146
  }
136
147
 
137
148
  /** Build a stack-trace-friendly diagnostic line from a violation's code and coordinates. */
138
- function formatDiagnostic(violation: import("./types").Violation | undefined): string {
149
+ function formatDiagnostic(violation: Violation | undefined): string {
139
150
  if (!violation) return "Policy violation";
140
151
  const location = "group" in violation ? ` at group ${violation.group} rule ${violation.rule}` : "";
141
152
  return `Policy violation: ${violation.code}${location}`;