@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/dist/index.d.ts +54 -44
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +315 -323
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bytes.ts +9 -9
- package/src/constants.ts +36 -175
- package/src/constraint.ts +7 -32
- package/src/descriptor-coder.ts +18 -20
- package/src/descriptor.ts +12 -11
- package/src/errors.ts +0 -3
- package/src/index.ts +7 -17
- package/src/operators.ts +114 -9
- package/src/path.ts +71 -0
- package/src/policy-builder.ts +17 -16
- package/src/policy-coder.ts +11 -41
- package/src/policy-enforcer.ts +22 -34
- package/src/policy-validator.ts +20 -23
- package/src/type-desc.ts +10 -10
- package/src/types.ts +70 -3
- package/src/validation-issue.ts +4 -4
package/src/constants.ts
CHANGED
|
@@ -28,7 +28,10 @@ export const DescriptorFormat = {
|
|
|
28
28
|
// Policy format
|
|
29
29
|
///////////////////////////////////////////////////////////////////////////
|
|
30
30
|
|
|
31
|
-
/**
|
|
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
|
|
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 }[]>(
|
|
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>(
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
{ key: "
|
|
157
|
-
{ key: "
|
|
158
|
-
{ key: "
|
|
159
|
-
{ key: "
|
|
160
|
-
{ key: "
|
|
161
|
-
{ key: "
|
|
162
|
-
{ key: "
|
|
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(
|
|
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(...
|
|
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
|
-
|
|
175
|
-
|
|
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 =
|
|
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>(
|
|
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
|
-
|
|
265
|
-
|
|
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,
|
|
2
|
-
import { Op, Scope, ContextProperty,
|
|
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
|
-
|
|
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.
|
|
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
|
///////////////////////////////////////////////////////////////////////////
|
package/src/descriptor-coder.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readU16, readU24 } from "./bytes";
|
|
2
|
-
import { DescriptorFormat as DF, TypeCode
|
|
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
|
|
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
|
-
|
|
88
|
+
closeIndex = i;
|
|
88
89
|
break;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
if (
|
|
93
|
+
if (closeIndex === -1) {
|
|
93
94
|
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '(' at position ${start}`);
|
|
94
95
|
}
|
|
95
|
-
if (
|
|
96
|
-
throw new CallciumError("MALFORMED_TYPE_STRING", `Unexpected characters after ')' at position ${
|
|
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 =
|
|
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
|
|
162
|
-
let
|
|
162
|
+
const closeIndex = baseEnd - 1;
|
|
163
|
+
let openIndex = closeIndex - 1;
|
|
163
164
|
// Walk back past digits (for static arrays).
|
|
164
|
-
while (
|
|
165
|
-
|
|
165
|
+
while (openIndex > start && input[openIndex] !== "[") {
|
|
166
|
+
openIndex--;
|
|
166
167
|
}
|
|
167
|
-
if (input[
|
|
168
|
-
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${
|
|
168
|
+
if (input[openIndex] !== "[") {
|
|
169
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closeIndex}`);
|
|
169
170
|
}
|
|
170
|
-
const innerStart =
|
|
171
|
-
const innerEnd =
|
|
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 =
|
|
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,
|
|
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
|
|
203
|
-
const lengthOffset = elemOffset +
|
|
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
|
|
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,
|