@callcium/sdk 0.9.0 → 0.10.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/constants.ts CHANGED
@@ -28,7 +28,10 @@ export const DescriptorFormat = {
28
28
  // Policy format
29
29
  ///////////////////////////////////////////////////////////////////////////
30
30
 
31
- /** Binary layout constants for the Callcium policy format. */
31
+ /** Byte width of a rule's data length field. */
32
+ const RULE_DATALENGTH_SIZE = 2;
33
+
34
+ /** Binary layout constants and normative limits for the Callcium policy format. */
32
35
  export const PolicyFormat = {
33
36
  VERSION: 0x02,
34
37
  VERSION_MASK: 0x0f,
@@ -50,7 +53,7 @@ export const PolicyFormat = {
50
53
  RULE_PATH_OFFSET: 4,
51
54
  PATH_STEP_SIZE: 2,
52
55
  RULE_OPCODE_SIZE: 1,
53
- RULE_DATALENGTH_SIZE: 2,
56
+ RULE_DATALENGTH_SIZE,
54
57
  RULE_FIXED_OVERHEAD: 7,
55
58
  RULE_MIN_SIZE: 9,
56
59
  HINT_HEADER_SIZE: 1,
@@ -76,6 +79,10 @@ export const PolicyFormat = {
76
79
  HINT_META_DYNAMIC_ARRAY: 0x4000,
77
80
  HINT_META_RESERVED_MASK: 0x3000,
78
81
  HINT_META_STRIDE_MASK: 0x0fff,
82
+ // Normative limits.
83
+ MAX_PATH_DEPTH: 32,
84
+ MAX_QUANTIFIED_ARRAY_LENGTH: 256,
85
+ MAX_SET_MEMBERS: Math.floor((256 ** RULE_DATALENGTH_SIZE - 1) / 32),
79
86
  } as const satisfies Record<string, number>;
80
87
 
81
88
  ///////////////////////////////////////////////////////////////////////////
@@ -91,7 +98,9 @@ type CodeMap<T extends readonly { readonly key: string; readonly code: number }[
91
98
  };
92
99
 
93
100
  /** Build a plain `{ KEY: code }` object from a table at runtime. */
94
- function buildCodeMap<T extends readonly { readonly key: string; readonly code: number }[]>(table: T): CodeMap<T> {
101
+ export function buildCodeMap<T extends readonly { readonly key: string; readonly code: number }[]>(
102
+ table: T,
103
+ ): CodeMap<T> {
95
104
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- derived from the same const table that defines the type.
96
105
  return Object.fromEntries(table.map((e) => [e.key, e.code])) as CodeMap<T>;
97
106
  }
@@ -111,7 +120,9 @@ export const Scope = buildCodeMap(SCOPE_TABLE);
111
120
  /** Display metadata for a scope code. */
112
121
  export type ScopeInfo = { label: string };
113
122
 
114
- const scopeByCode = new Map<number, ScopeInfo>(SCOPE_TABLE.map((e) => [e.code, { label: e.label }]));
123
+ const scopeByCode: ReadonlyMap<number, ScopeInfo> = new Map<number, ScopeInfo>(
124
+ SCOPE_TABLE.map((e) => [e.code, { label: e.label }]),
125
+ );
115
126
 
116
127
  /**
117
128
  * Map a scope code to its display label.
@@ -151,40 +162,29 @@ export const TypeCode = {
151
162
  // Context property IDs
152
163
  ///////////////////////////////////////////////////////////////////////////
153
164
 
154
- const CTX_PROP_TABLE = [
155
- { key: "MSG_SENDER", code: 0x0000, label: "msg.sender", typeCode: TypeCode.ADDRESS },
156
- { key: "MSG_VALUE", code: 0x0001, label: "msg.value", typeCode: TypeCode.UINT_MAX },
157
- { key: "BLOCK_TIMESTAMP", code: 0x0002, label: "block.timestamp", typeCode: TypeCode.UINT_MAX },
158
- { key: "BLOCK_NUMBER", code: 0x0003, label: "block.number", typeCode: TypeCode.UINT_MAX },
159
- { key: "CHAIN_ID", code: 0x0004, label: "block.chainid", typeCode: TypeCode.UINT_MAX },
160
- { key: "TX_ORIGIN", code: 0x0005, label: "tx.origin", typeCode: TypeCode.ADDRESS },
161
- { key: "BASE_FEE", code: 0x0006, label: "block.basefee", typeCode: TypeCode.UINT_MAX },
162
- { key: "GAS_PRICE", code: 0x0007, label: "tx.gasprice", typeCode: TypeCode.UINT_MAX },
165
+ // oxfmt-ignore
166
+ const CTX_PROPERTY_TABLE = [
167
+ { key: "MSG_SENDER", code: 0x0000, label: "msg.sender", contextKey: "msgSender", typeCode: TypeCode.ADDRESS },
168
+ { key: "MSG_VALUE", code: 0x0001, label: "msg.value", contextKey: "msgValue", typeCode: TypeCode.UINT_MAX },
169
+ { key: "BLOCK_TIMESTAMP", code: 0x0002, label: "block.timestamp", contextKey: "blockTimestamp", typeCode: TypeCode.UINT_MAX },
170
+ { key: "BLOCK_NUMBER", code: 0x0003, label: "block.number", contextKey: "blockNumber", typeCode: TypeCode.UINT_MAX },
171
+ { key: "CHAIN_ID", code: 0x0004, label: "block.chainid", contextKey: "chainId", typeCode: TypeCode.UINT_MAX },
172
+ { key: "TX_ORIGIN", code: 0x0005, label: "tx.origin", contextKey: "txOrigin", typeCode: TypeCode.ADDRESS },
173
+ { key: "BASE_FEE", code: 0x0006, label: "block.basefee", contextKey: "baseFee", typeCode: TypeCode.UINT_MAX },
174
+ { key: "GAS_PRICE", code: 0x0007, label: "tx.gasprice", contextKey: "gasPrice", typeCode: TypeCode.UINT_MAX },
163
175
  ] as const;
164
176
 
165
177
  /** Well-known context property IDs for context-scope rules. */
166
- export const ContextProperty = buildCodeMap(CTX_PROP_TABLE);
178
+ export const ContextProperty = buildCodeMap(CTX_PROPERTY_TABLE);
167
179
 
168
180
  /** Maximum valid context property ID. */
169
- export const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROP_TABLE.map((e) => e.code));
181
+ export const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROPERTY_TABLE.map((e) => e.code));
170
182
 
171
183
  /** Display metadata for a context property code. */
172
184
  export type ContextPropertyInfo = { label: string; contextKey: keyof import("./types").Context; typeCode: number };
173
185
 
174
- /** Maps each context property table key to its camelCase Context key. */
175
- const CTX_KEY_MAP: Record<(typeof CTX_PROP_TABLE)[number]["key"], keyof import("./types").Context> = {
176
- MSG_SENDER: "msgSender",
177
- MSG_VALUE: "msgValue",
178
- BLOCK_TIMESTAMP: "blockTimestamp",
179
- BLOCK_NUMBER: "blockNumber",
180
- CHAIN_ID: "chainId",
181
- TX_ORIGIN: "txOrigin",
182
- BASE_FEE: "baseFee",
183
- GAS_PRICE: "gasPrice",
184
- };
185
-
186
- const ctxPropByCode = new Map<number, ContextPropertyInfo>(
187
- CTX_PROP_TABLE.map((e) => [e.code, { label: e.label, contextKey: CTX_KEY_MAP[e.key], typeCode: e.typeCode }]),
186
+ const ctxPropertyByCode: ReadonlyMap<number, ContextPropertyInfo> = new Map<number, ContextPropertyInfo>(
187
+ CTX_PROPERTY_TABLE.map((e) => [e.code, { label: e.label, contextKey: e.contextKey, typeCode: e.typeCode }]),
188
188
  );
189
189
 
190
190
  /**
@@ -194,22 +194,11 @@ const ctxPropByCode = new Map<number, ContextPropertyInfo>(
194
194
  * @throws {CallciumError} If the code is not a recognised context property.
195
195
  */
196
196
  export function lookupContextProperty(code: number): ContextPropertyInfo {
197
- const info = ctxPropByCode.get(code);
197
+ const info = ctxPropertyByCode.get(code);
198
198
  if (!info) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ${code}`);
199
199
  return info;
200
200
  }
201
201
 
202
- ///////////////////////////////////////////////////////////////////////////
203
- // Protocol limits
204
- ///////////////////////////////////////////////////////////////////////////
205
-
206
- /** Protocol-imposed safety limits for path depth and quantifier array size. */
207
- export const Limits = {
208
- MAX_PATH_DEPTH: 32,
209
- MAX_QUANTIFIED_ARRAY_LENGTH: 256,
210
- MAX_SET_MEMBERS: Math.floor((256 ** PolicyFormat.RULE_DATALENGTH_SIZE - 1) / 32),
211
- } as const satisfies Record<string, number>;
212
-
213
202
  ///////////////////////////////////////////////////////////////////////////
214
203
  // Operator codes
215
204
  ///////////////////////////////////////////////////////////////////////////
@@ -244,7 +233,9 @@ export const Op: CodeMap<typeof OP_TABLE> & { readonly NOT: 0x80 } = { ...buildC
244
233
  /** Display metadata for an operator code. */
245
234
  export type OpInfo = { label: string; operands: Operands };
246
235
 
247
- const opByCode = new Map<number, OpInfo>(OP_TABLE.map((e) => [e.code, { label: e.label, operands: e.operands }]));
236
+ const opByCode: ReadonlyMap<number, OpInfo> = new Map<number, OpInfo>(
237
+ OP_TABLE.map((e) => [e.code, { label: e.label, operands: e.operands }]),
238
+ );
248
239
 
249
240
  /**
250
241
  * Map an operator code to its display label. Strips the NOT flag automatically.
@@ -260,137 +251,7 @@ export function lookupOp(code: number): OpInfo {
260
251
  return info;
261
252
  }
262
253
 
263
- /**
264
- * Check whether an operator's data payload has the correct length.
265
- * @param opBase - Base operator code with the NOT flag stripped.
266
- * @param dataLength - Byte length of the operator's data payload.
267
- * @returns True if the data length is valid for the given operator.
268
- */
269
- export function isValidOperatorData(opBase: number, dataLength: number): boolean {
270
- const operands = opByCode.get(opBase)?.operands;
271
- if (operands === "single") return dataLength === 32;
272
- if (operands === "range") return dataLength === 64;
273
- if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
274
- return false;
275
- }
276
-
277
- ///////////////////////////////////////////////////////////////////////////
278
- // Quantifier steps
279
- ///////////////////////////////////////////////////////////////////////////
280
-
281
- const QUANTIFIER_TABLE = [
282
- { key: "ALL", code: 0xffff, label: "all" },
283
- { key: "ANY", code: 0xfffe, label: "any" },
284
- ] as const;
285
-
286
- /** Reserved path step values that trigger quantified evaluation over array elements. */
287
- export const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
288
-
289
- /** Display metadata for a quantifier step. */
290
- export type QuantifierInfo = { label: string };
291
-
292
- const quantifierByCode = new Map<number, QuantifierInfo>(QUANTIFIER_TABLE.map((e) => [e.code, { label: e.label }]));
293
-
294
- /** Check whether a path step is a quantifier (ALL or ANY). */
295
- export function isQuantifier(step: number): boolean {
296
- return step >= Quantifier.ANY;
297
- }
298
-
299
- /**
300
- * Map a quantifier path step to its display label.
301
- * @param code - Quantifier step value.
302
- * @returns Display metadata for the quantifier.
303
- * @throws {CallciumError} If the code is not a recognised quantifier.
304
- */
305
- export function lookupQuantifier(code: number): QuantifierInfo {
306
- const info = quantifierByCode.get(code);
307
- if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
308
- return info;
309
- }
310
-
311
- /** Structural category for a descriptor type code. */
312
- export type TypeClass = "elementary" | "tuple" | "staticArray" | "dynamicArray";
313
-
314
- /** Structural classification without label. */
315
- export type TypeClassInfo = { typeClass: TypeClass; isDynamic: boolean };
316
-
317
- /** Display metadata for a descriptor type code. */
318
- export type TypeCodeInfo = TypeClassInfo & { label: string };
319
-
320
- /** Throw an UNKNOWN_TYPE_CODE error. */
321
- function unknownTypeCode(code: number): never {
322
- throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
323
- }
324
-
325
- // Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
326
- const ELEMENTARY = { typeClass: "elementary", isDynamic: false } as const;
327
- const ELEMENTARY_DYN = { typeClass: "elementary", isDynamic: true } as const;
328
- const STATIC_ARR = { typeClass: "staticArray", isDynamic: false } as const;
329
- const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
330
- const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
331
-
332
- /**
333
- * Classify a type code into its structural category and dynamism.
334
- * Lightweight variant of `lookupTypeCode` that skips label computation.
335
- * @param code - A single-byte descriptor type code.
336
- * @returns Type class and whether the type is ABI-dynamic.
337
- * @throws {CallciumError} If the code is not a recognised type code.
338
- */
339
- export function classifyTypeCode(code: number): TypeClassInfo {
340
- if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
341
- if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
342
- if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
343
- if (code >= 0x44 && code <= 0x4f) unknownTypeCode(code);
344
- if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
345
- if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYN;
346
- if (code >= 0x72 && code <= 0x7f) unknownTypeCode(code);
347
- if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
348
- if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
349
- if (code >= 0x82 && code <= 0x8f) unknownTypeCode(code);
350
- if (code === TypeCode.TUPLE) return TUPLE;
351
- unknownTypeCode(code);
352
- }
353
-
354
- /**
355
- * Determine whether a type code names a value an operator can read: a scalar word or a declared
356
- * length. Tuples, static arrays, and undefined codes address nothing.
357
- * @param code - A single-byte descriptor type code.
358
- * @returns True when an operator can be applied to a target of this type.
359
- */
360
- export function isAddressableTarget(code: number): boolean {
361
- let info: TypeClassInfo;
362
- try {
363
- // An undefined code is reported by a throw, and addresses nothing either way.
364
- info = classifyTypeCode(code);
365
- } catch {
366
- return false;
367
- }
368
- return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
369
- }
370
-
371
- /** Compute the ABI type label for a type code. */
372
- function typeCodeLabel(code: number): string {
373
- if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
374
- if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
375
- if (code === TypeCode.ADDRESS) return "address";
376
- if (code === TypeCode.BOOL) return "bool";
377
- if (code === TypeCode.FUNCTION) return "function";
378
- if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX)
379
- return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
380
- if (code === TypeCode.BYTES) return "bytes";
381
- if (code === TypeCode.STRING) return "string";
382
- if (code === TypeCode.STATIC_ARRAY) return "T[k]";
383
- if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
384
- if (code === TypeCode.TUPLE) return "tuple";
385
- return `0x${code.toString(16).padStart(2, "0")}`;
386
- }
387
-
388
- /**
389
- * Map a raw type code byte to its ABI type label, structural category, and dynamism.
390
- * @param code - A single-byte descriptor type code.
391
- * @returns Label, type class, and whether the type is ABI-dynamic.
392
- * @throws {CallciumError} If the code is not a recognised type code.
393
- */
394
- export function lookupTypeCode(code: number): TypeCodeInfo {
395
- return { label: typeCodeLabel(code), ...classifyTypeCode(code) };
254
+ /** Operand count category for an operator code, or undefined when unrecognised. */
255
+ export function operandsOf(opBase: number): Operands | undefined {
256
+ return opByCode.get(opBase)?.operands;
396
257
  }
package/src/constraint.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { bytesToHex, toAddress, writeBE16 } from "./bytes";
2
- import { Op, Scope, ContextProperty, Limits, MAX_CONTEXT_PROPERTY_ID } from "./constants";
1
+ import { bytesToHex, hexToBytes, toAddress } from "./bytes";
2
+ import { PolicyFormat, Op, Scope, ContextProperty, MAX_CONTEXT_PROPERTY_ID } from "./constants";
3
3
  import { CallciumError } from "./errors";
4
+ import { encodePath } from "./path";
4
5
 
5
6
  import type { Hex, Constraint } from "./types";
6
7
 
@@ -27,10 +28,7 @@ function encodeWord(value: ScalarValue): Uint8Array {
27
28
 
28
29
  if (typeof value === "string") {
29
30
  // Address encoding: validate 20-byte hex and right-align into 32 bytes.
30
- const body = addressBody(value);
31
- for (let i = 0; i < 20; i++) {
32
- word[12 + i] = parseInt(body.substring(i * 2, i * 2 + 2), 16);
33
- }
31
+ word.set(hexToBytes(addressBody(value)), 12);
34
32
  return word;
35
33
  }
36
34
 
@@ -71,9 +69,6 @@ function checkContextPropertyId(contextPropertyId: number): number {
71
69
  return contextPropertyId;
72
70
  }
73
71
 
74
- /** Largest member count whose 32-byte words still fit a rule's data length field. */
75
- export const MAX_SET_MEMBERS = Limits.MAX_SET_MEMBERS;
76
-
77
72
  /** Convert values to bigint, sort ascending (unsigned), deduplicate, and pack as set payload. */
78
73
  function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
79
74
  const bigs = values.map((v) => {
@@ -84,20 +79,13 @@ function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
84
79
  return BigInt("0x" + addressBody(v));
85
80
  });
86
81
 
87
- bigs.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
88
-
89
- const deduped: bigint[] = [];
90
- for (const value of bigs) {
91
- if (deduped.length === 0 || deduped[deduped.length - 1] !== value) {
92
- deduped.push(value);
93
- }
94
- }
82
+ const deduped = [...new Set(bigs)].toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0));
95
83
 
96
84
  if (deduped.length === 0) {
97
85
  throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
98
86
  }
99
- if (deduped.length > MAX_SET_MEMBERS) {
100
- throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${MAX_SET_MEMBERS} values`);
87
+ if (deduped.length > PolicyFormat.MAX_SET_MEMBERS) {
88
+ throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${PolicyFormat.MAX_SET_MEMBERS} values`);
101
89
  }
102
90
 
103
91
  const buffer = new Uint8Array(1 + deduped.length * 32);
@@ -108,19 +96,6 @@ function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
108
96
  return bytesToHex(buffer);
109
97
  }
110
98
 
111
- ///////////////////////////////////////////////////////////////////////////
112
- // Path encoding
113
- ///////////////////////////////////////////////////////////////////////////
114
-
115
- /** Encode a sequence of uint16 path steps as a big-endian hex string. */
116
- function encodePath(steps: readonly number[]): Hex {
117
- const buffer = new Uint8Array(steps.length * 2);
118
- for (let i = 0; i < steps.length; i++) {
119
- writeBE16(buffer, i * 2, steps[i]!);
120
- }
121
- return bytesToHex(buffer);
122
- }
123
-
124
99
  ///////////////////////////////////////////////////////////////////////////
125
100
  // ConstraintBuilder
126
101
  ///////////////////////////////////////////////////////////////////////////
@@ -1,7 +1,8 @@
1
1
  import { readU16, readU24 } from "./bytes";
2
- import { DescriptorFormat as DF, TypeCode, lookupTypeCode, classifyTypeCode } from "./constants";
2
+ import { DescriptorFormat as DF, TypeCode } from "./constants";
3
3
  import { Descriptor } from "./descriptor";
4
4
  import { CallciumError } from "./errors";
5
+ import { classifyTypeCode, lookupTypeCode } from "./operators";
5
6
  import { address, array, bool, bytes, bytesN, function_, intN, string_, tuple, uintN } from "./type-desc";
6
7
 
7
8
  import type { DecodedParam, Hex } from "./types";
@@ -78,26 +79,26 @@ function parseTuple(input: string, start: number, end: number): Uint8Array {
78
79
  throw new CallciumError("MALFORMED_TYPE_STRING", `Expected '(' at position ${start}`);
79
80
  }
80
81
  let depth = 0;
81
- let closePos = -1;
82
+ let closeIndex = -1;
82
83
  for (let i = start; i < end; i++) {
83
84
  if (input[i] === "(") depth++;
84
85
  else if (input[i] === ")") {
85
86
  depth--;
86
87
  if (depth === 0) {
87
- closePos = i;
88
+ closeIndex = i;
88
89
  break;
89
90
  }
90
91
  }
91
92
  }
92
- if (closePos === -1) {
93
+ if (closeIndex === -1) {
93
94
  throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '(' at position ${start}`);
94
95
  }
95
- if (closePos !== end - 1) {
96
- throw new CallciumError("MALFORMED_TYPE_STRING", `Unexpected characters after ')' at position ${closePos + 1}`);
96
+ if (closeIndex !== end - 1) {
97
+ throw new CallciumError("MALFORMED_TYPE_STRING", `Unexpected characters after ')' at position ${closeIndex + 1}`);
97
98
  }
98
99
 
99
100
  const innerStart = start + 1;
100
- const innerEnd = closePos;
101
+ const innerEnd = closeIndex;
101
102
  const commas = commaPositions(input, innerStart, innerEnd);
102
103
 
103
104
  const segments: Array<[number, number]> = [];
@@ -158,20 +159,20 @@ function parseType(input: string, start: number, end: number): Uint8Array {
158
159
  let baseEnd = end;
159
160
 
160
161
  while (baseEnd > start && input[baseEnd - 1] === "]") {
161
- const closePos = baseEnd - 1;
162
- let openPos = closePos - 1;
162
+ const closeIndex = baseEnd - 1;
163
+ let openIndex = closeIndex - 1;
163
164
  // Walk back past digits (for static arrays).
164
- while (openPos > start && input[openPos] !== "[") {
165
- openPos--;
165
+ while (openIndex > start && input[openIndex] !== "[") {
166
+ openIndex--;
166
167
  }
167
- if (input[openPos] !== "[") {
168
- throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closePos}`);
168
+ if (input[openIndex] !== "[") {
169
+ throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closeIndex}`);
169
170
  }
170
- const innerStart = openPos + 1;
171
- const innerEnd = closePos;
171
+ const innerStart = openIndex + 1;
172
+ const innerEnd = closeIndex;
172
173
  // A dynamic array spells no length at all.
173
174
  suffixes.unshift(innerStart === innerEnd ? undefined : parseUint(input, innerStart, innerEnd));
174
- baseEnd = openPos;
175
+ baseEnd = openIndex;
175
176
  }
176
177
 
177
178
  // Every `[` a base carries belongs to a suffix the scan above already took; one left behind is
@@ -219,10 +220,7 @@ function fromTypes(typesCsv: string): Uint8Array {
219
220
 
220
221
  const paramDescs = segments.map(([start, end]) => parseType(typesCsv, start, end));
221
222
  if (paramDescs.length > DF.MAX_PARAMS) {
222
- throw new CallciumError(
223
- "TOO_MANY_PARAMS",
224
- `Parameter count ${paramDescs.length} exceeds maximum ${DF.MAX_PARAMS}.`,
225
- );
223
+ throw new CallciumError("TOO_MANY_PARAMS", `Parameter count ${paramDescs.length} exceeds maximum ${DF.MAX_PARAMS}`);
226
224
  }
227
225
  const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
228
226
  const result = new Uint8Array(DF.HEADER_SIZE + totalBytes);
package/src/descriptor.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readU16, readU24, writeBE16, writeBE32 } from "./bytes";
2
- import { DescriptorFormat as DF, PolicyFormat as PF, Quantifier, TypeCode, isQuantifier } from "./constants";
2
+ import { DescriptorFormat as DF, PolicyFormat as PF, TypeCode } from "./constants";
3
3
  import { CallciumError } from "./errors";
4
+ import { Quantifier, isQuantifier } from "./path";
4
5
 
5
6
  ///////////////////////////////////////////////////////////////////////////
6
7
  // Public types
@@ -76,7 +77,7 @@ function paramCount(desc: Uint8Array): number {
76
77
  function paramOffset(desc: Uint8Array, index: number): number {
77
78
  const count = desc[1]!;
78
79
  if (index >= count) {
79
- throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count}).`);
80
+ throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})`);
80
81
  }
81
82
  let cursor = DF.HEADER_SIZE;
82
83
  for (let i = 0; i < index; i++) {
@@ -130,7 +131,7 @@ function typeAt(desc: Uint8Array, steps: number[]): TypeInfo {
130
131
  */
131
132
  function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quantifiedStaticLength: number } {
132
133
  if (steps.length === 0) {
133
- throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
134
+ throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
134
135
  }
135
136
 
136
137
  const paramIndex = steps[0]!;
@@ -138,7 +139,7 @@ function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quan
138
139
  if (paramIndex >= count) {
139
140
  throw new CallciumError(
140
141
  "PARAM_INDEX_OUT_OF_BOUNDS",
141
- `Param index ${paramIndex} out of range (paramCount=${count}).`,
142
+ `Param index ${paramIndex} out of range (paramCount=${count})`,
142
143
  );
143
144
  }
144
145
 
@@ -154,7 +155,7 @@ function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quan
154
155
  if (step >= fields) {
155
156
  throw new CallciumError(
156
157
  "TUPLE_FIELD_OUT_OF_BOUNDS",
157
- `Tuple field index ${step} out of range (tuple has ${fields} fields).`,
158
+ `Tuple field index ${step} out of range (tuple has ${fields} fields)`,
158
159
  );
159
160
  }
160
161
  cursor = tupleFieldOffset(desc, cursor, step);
@@ -169,14 +170,14 @@ function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quan
169
170
  if (step >= length) {
170
171
  throw new CallciumError(
171
172
  "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
172
- `Array index ${step} out of range (length=${length}).`,
173
+ `Array index ${step} out of range (length=${length})`,
173
174
  );
174
175
  }
175
176
  }
176
177
  }
177
178
  cursor = arrayElementOffset(cursor);
178
179
  } else {
179
- throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}.`);
180
+ throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}`);
180
181
  }
181
182
  }
182
183
 
@@ -199,8 +200,8 @@ function tupleFieldCount(desc: Uint8Array, offset: number): number {
199
200
  */
200
201
  function staticArrayLength(desc: Uint8Array, offset: number): number {
201
202
  const elemOffset = arrayElementOffset(offset);
202
- const elemLen = nodeLength(desc, elemOffset);
203
- const lengthOffset = elemOffset + elemLen;
203
+ const elemNodeLength = nodeLength(desc, elemOffset);
204
+ const lengthOffset = elemOffset + elemNodeLength;
204
205
  return readU16(desc, lengthOffset);
205
206
  }
206
207
 
@@ -214,7 +215,7 @@ function arrayElementOffset(offset: number): number {
214
215
 
215
216
  /** Build the error for a path step that does not resolve to a hop-chain target. */
216
217
  function uncompilablePath(stepIndex: number): CallciumError {
217
- return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target.`);
218
+ return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target`);
218
219
  }
219
220
 
220
221
  /** Return the combined head slot span of the `count` nodes at `offset`, and the offset of the node past them. */
@@ -353,7 +354,7 @@ function encodeHint(walk: HintWalk, targetMeta: number, typeCode: number, depth:
353
354
  */
354
355
  function compileHint(desc: Uint8Array, steps: number[]): Uint8Array {
355
356
  if (steps.length === 0) {
356
- throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
357
+ throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
357
358
  }
358
359
 
359
360
  const argIndex = steps[0]!;
package/src/errors.ts CHANGED
@@ -88,7 +88,6 @@ export class CallciumError extends Error {
88
88
  super(`${prefix}${message}`);
89
89
  this.name = "CallciumError";
90
90
  this.code = code;
91
- Object.setPrototypeOf(this, CallciumError.prototype);
92
91
  }
93
92
  }
94
93
 
@@ -106,7 +105,6 @@ export class ValidationError extends CallciumError {
106
105
  super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
107
106
  this.name = "ValidationError";
108
107
  this.issues = issues;
109
- Object.setPrototypeOf(this, ValidationError.prototype);
110
108
  }
111
109
  }
112
110
 
@@ -126,7 +124,6 @@ export class PolicyViolationError extends Error {
126
124
  super(formatDiagnostic(violations[0]));
127
125
  this.name = "PolicyViolationError";
128
126
  this.violations = violations;
129
- Object.setPrototypeOf(this, PolicyViolationError.prototype);
130
127
  }
131
128
  }
132
129
 
package/src/index.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  ///////////////////////////////////////////////////////////////////////////
4
4
 
5
5
  export { PolicyBuilder } from "./policy-builder";
6
- export { PolicyCoder, parsePathSteps } from "./policy-coder";
6
+ export { PolicyCoder } from "./policy-coder";
7
+ export { isQuantifier, lookupQuantifier, parsePathSteps, Quantifier } from "./path";
8
+ export type { QuantifierInfo } from "./path";
7
9
  export { isOpAllowed, PolicyValidator } from "./policy-validator";
8
10
  export {
9
11
  arg,
@@ -41,29 +43,16 @@ export {
41
43
  Op,
42
44
  TypeCode,
43
45
  PolicyFormat,
44
- Quantifier,
45
46
  Scope,
46
47
  ContextProperty,
47
- Limits,
48
48
  MAX_CONTEXT_PROPERTY_ID,
49
49
  lookupOp,
50
50
  lookupScope,
51
51
  lookupContextProperty,
52
- lookupQuantifier,
53
- lookupTypeCode,
54
- isQuantifier,
55
- } from "./constants";
56
- export { isLengthOp } from "./operators";
57
- export type {
58
- Operands,
59
- OpInfo,
60
- ScopeInfo,
61
- ContextPropertyInfo,
62
- QuantifierInfo,
63
- TypeCodeInfo,
64
- TypeClassInfo,
65
- TypeClass,
66
52
  } from "./constants";
53
+ export { isLengthOp, lookupTypeCode } from "./operators";
54
+ export type { TypeCodeInfo, TypeClassInfo, TypeClass } from "./operators";
55
+ export type { Operands, OpInfo, ScopeInfo, ContextPropertyInfo } from "./constants";
67
56
 
68
57
  ///////////////////////////////////////////////////////////////////////////
69
58
  // Types
@@ -81,6 +70,7 @@ export type {
81
70
  DecodedRule,
82
71
  DecodedParam,
83
72
  Issue,
73
+ IssueCode,
84
74
  IssueSeverity,
85
75
  IssueCategory,
86
76
  Context,