@callcium/sdk 0.8.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 +74 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +411 -330
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bytes.ts +9 -9
- package/src/constants.ts +37 -175
- package/src/constraint.ts +24 -31
- package/src/descriptor-coder.ts +18 -20
- package/src/descriptor.ts +12 -11
- package/src/errors.ts +0 -3
- package/src/index.ts +8 -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 +22 -41
- package/src/policy-enforcer.ts +90 -35
- package/src/policy-validator.ts +58 -25
- package/src/type-desc.ts +10 -10
- package/src/types.ts +79 -5
- package/src/validation-issue.ts +18 -4
package/src/operators.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Op, TypeCode } from "./constants";
|
|
1
|
+
import { Op, TypeCode, operandsOf } from "./constants";
|
|
2
2
|
import { CallciumError } from "./errors";
|
|
3
3
|
|
|
4
4
|
/** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
|
|
@@ -28,7 +28,7 @@ export function isLeftAligned(typeCode: number): boolean {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type
|
|
31
|
+
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type.
|
|
32
32
|
* A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
|
|
33
33
|
* unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
|
|
34
34
|
* comparison use the canonical ABI value rather than the raw bytes.
|
|
@@ -103,14 +103,14 @@ function compareTyped(a: bigint, b: bigint, typeCode: number): number {
|
|
|
103
103
|
/** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
|
|
104
104
|
function isIn(value: bigint, data: Uint8Array): boolean {
|
|
105
105
|
const count = data.length / 32;
|
|
106
|
-
let
|
|
107
|
-
let
|
|
108
|
-
while (
|
|
109
|
-
const mid = (
|
|
106
|
+
let low = 0;
|
|
107
|
+
let high = count - 1;
|
|
108
|
+
while (low <= high) {
|
|
109
|
+
const mid = (low + high) >>> 1;
|
|
110
110
|
const elem = toBigInt(data, mid * 32);
|
|
111
111
|
if (value === elem) return true;
|
|
112
|
-
if (value < elem)
|
|
113
|
-
else
|
|
112
|
+
if (value < elem) high = mid - 1;
|
|
113
|
+
else low = mid + 1;
|
|
114
114
|
}
|
|
115
115
|
return false;
|
|
116
116
|
}
|
|
@@ -196,9 +196,114 @@ export function applyOperator(
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
default:
|
|
199
|
-
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}
|
|
199
|
+
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}`);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
const negate = (opCode & Op.NOT) !== 0;
|
|
203
203
|
return negate ? !result : result;
|
|
204
204
|
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check whether an operator's data payload has the correct length.
|
|
208
|
+
* @param opBase - Base operator code with the NOT flag stripped.
|
|
209
|
+
* @param dataLength - Byte length of the operator's data payload.
|
|
210
|
+
* @returns True if the data length is valid for the given operator.
|
|
211
|
+
*/
|
|
212
|
+
export function isValidOperatorData(opBase: number, dataLength: number): boolean {
|
|
213
|
+
const operands = operandsOf(opBase);
|
|
214
|
+
if (operands === "single") return dataLength === 32;
|
|
215
|
+
if (operands === "range") return dataLength === 64;
|
|
216
|
+
if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
///////////////////////////////////////////////////////////////////////////
|
|
221
|
+
// Type code classification
|
|
222
|
+
///////////////////////////////////////////////////////////////////////////
|
|
223
|
+
|
|
224
|
+
/** Structural category for a descriptor type code. */
|
|
225
|
+
export type TypeClass = "elementary" | "tuple" | "staticArray" | "dynamicArray";
|
|
226
|
+
|
|
227
|
+
/** Structural classification without label. */
|
|
228
|
+
export type TypeClassInfo = { typeClass: TypeClass; isDynamic: boolean };
|
|
229
|
+
|
|
230
|
+
/** Display metadata for a descriptor type code. */
|
|
231
|
+
export type TypeCodeInfo = TypeClassInfo & { label: string };
|
|
232
|
+
|
|
233
|
+
/** Throw an UNKNOWN_TYPE_CODE error. */
|
|
234
|
+
function unknownTypeCode(code: number): never {
|
|
235
|
+
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
|
|
239
|
+
const ELEMENTARY = { typeClass: "elementary", isDynamic: false } as const;
|
|
240
|
+
const ELEMENTARY_DYNAMIC = { typeClass: "elementary", isDynamic: true } as const;
|
|
241
|
+
const STATIC_ARR = { typeClass: "staticArray", isDynamic: false } as const;
|
|
242
|
+
const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
|
|
243
|
+
const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Classify a type code into its structural category and dynamism.
|
|
247
|
+
* Lightweight variant of `lookupTypeCode` that skips label computation.
|
|
248
|
+
* @param code - A single-byte descriptor type code.
|
|
249
|
+
* @returns Type class and whether the type is ABI-dynamic.
|
|
250
|
+
* @throws {CallciumError} If the code is not a recognised type code.
|
|
251
|
+
*/
|
|
252
|
+
export function classifyTypeCode(code: number): TypeClassInfo {
|
|
253
|
+
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
|
|
254
|
+
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
|
|
255
|
+
if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
|
|
256
|
+
if (code >= 0x44 && code <= 0x4f) unknownTypeCode(code);
|
|
257
|
+
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
|
|
258
|
+
if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYNAMIC;
|
|
259
|
+
if (code >= 0x72 && code <= 0x7f) unknownTypeCode(code);
|
|
260
|
+
if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
|
|
261
|
+
if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
|
|
262
|
+
if (code >= 0x82 && code <= 0x8f) unknownTypeCode(code);
|
|
263
|
+
if (code === TypeCode.TUPLE) return TUPLE;
|
|
264
|
+
unknownTypeCode(code);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Determine whether a type code names a value an operator can read: a scalar word or a declared
|
|
269
|
+
* length. Tuples, static arrays, and undefined codes address nothing.
|
|
270
|
+
* @param code - A single-byte descriptor type code.
|
|
271
|
+
* @returns True when an operator can be applied to a target of this type.
|
|
272
|
+
*/
|
|
273
|
+
export function isAddressableTarget(code: number): boolean {
|
|
274
|
+
let info: TypeClassInfo;
|
|
275
|
+
try {
|
|
276
|
+
// An undefined code is reported by a throw, and addresses nothing either way.
|
|
277
|
+
info = classifyTypeCode(code);
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Compute the ABI type label for a type code. */
|
|
285
|
+
function typeCodeLabel(code: number): string {
|
|
286
|
+
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
|
|
287
|
+
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
|
|
288
|
+
if (code === TypeCode.ADDRESS) return "address";
|
|
289
|
+
if (code === TypeCode.BOOL) return "bool";
|
|
290
|
+
if (code === TypeCode.FUNCTION) return "function";
|
|
291
|
+
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX)
|
|
292
|
+
return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
|
|
293
|
+
if (code === TypeCode.BYTES) return "bytes";
|
|
294
|
+
if (code === TypeCode.STRING) return "string";
|
|
295
|
+
if (code === TypeCode.STATIC_ARRAY) return "T[k]";
|
|
296
|
+
if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
|
|
297
|
+
if (code === TypeCode.TUPLE) return "tuple";
|
|
298
|
+
return `0x${code.toString(16).padStart(2, "0")}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Map a raw type code byte to its ABI type label, structural category, and dynamism.
|
|
303
|
+
* @param code - A single-byte descriptor type code.
|
|
304
|
+
* @returns Label, type class, and whether the type is ABI-dynamic.
|
|
305
|
+
* @throws {CallciumError} If the code is not a recognised type code.
|
|
306
|
+
*/
|
|
307
|
+
export function lookupTypeCode(code: number): TypeCodeInfo {
|
|
308
|
+
return { label: typeCodeLabel(code), ...classifyTypeCode(code) };
|
|
309
|
+
}
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { bytesToHex, hexToBytes, readU16, writeBE16 } from "./bytes";
|
|
2
|
+
import { buildCodeMap, PolicyFormat as PF } from "./constants";
|
|
3
|
+
import { CallciumError } from "./errors";
|
|
4
|
+
|
|
5
|
+
import type { Hex } from "./types";
|
|
6
|
+
|
|
7
|
+
///////////////////////////////////////////////////////////////////////////
|
|
8
|
+
// Quantifier steps
|
|
9
|
+
///////////////////////////////////////////////////////////////////////////
|
|
10
|
+
|
|
11
|
+
const QUANTIFIER_TABLE = [
|
|
12
|
+
{ key: "ALL", code: 0xffff, label: "all" },
|
|
13
|
+
{ key: "ANY", code: 0xfffe, label: "any" },
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
/** Reserved path step values that trigger quantified evaluation over array elements. */
|
|
17
|
+
export const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
|
|
18
|
+
|
|
19
|
+
/** Display metadata for a quantifier step. */
|
|
20
|
+
export type QuantifierInfo = { label: string };
|
|
21
|
+
|
|
22
|
+
const quantifierByCode: ReadonlyMap<number, QuantifierInfo> = new Map<number, QuantifierInfo>(
|
|
23
|
+
QUANTIFIER_TABLE.map((e) => [e.code, { label: e.label }]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/** Check whether a path step is a quantifier (ALL or ANY). */
|
|
27
|
+
export function isQuantifier(step: number): boolean {
|
|
28
|
+
return step >= Quantifier.ANY;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Map a quantifier path step to its display label.
|
|
33
|
+
* @param code - Quantifier step value.
|
|
34
|
+
* @returns Display metadata for the quantifier.
|
|
35
|
+
* @throws {CallciumError} If the code is not a recognised quantifier.
|
|
36
|
+
*/
|
|
37
|
+
export function lookupQuantifier(code: number): QuantifierInfo {
|
|
38
|
+
const info = quantifierByCode.get(code);
|
|
39
|
+
if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
|
|
40
|
+
return info;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
///////////////////////////////////////////////////////////////////////////
|
|
44
|
+
// Path steps
|
|
45
|
+
///////////////////////////////////////////////////////////////////////////
|
|
46
|
+
|
|
47
|
+
/** Encode a sequence of uint16 path steps as a big-endian hex string. */
|
|
48
|
+
export function encodePath(steps: readonly number[]): Hex {
|
|
49
|
+
const buffer = new Uint8Array(steps.length * 2);
|
|
50
|
+
for (let i = 0; i < steps.length; i++) {
|
|
51
|
+
writeBE16(buffer, i * 2, steps[i]!);
|
|
52
|
+
}
|
|
53
|
+
return bytesToHex(buffer);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse a BE16-encoded hex path into an array of step values.
|
|
58
|
+
* @param path - 0x-prefixed hex string containing BE16-encoded path steps.
|
|
59
|
+
* @returns Array of numeric step values.
|
|
60
|
+
*/
|
|
61
|
+
export function parsePathSteps(path: Hex): number[] {
|
|
62
|
+
const bytes = hexToBytes(path);
|
|
63
|
+
if (bytes.length % PF.PATH_STEP_SIZE !== 0) {
|
|
64
|
+
throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
|
|
65
|
+
}
|
|
66
|
+
const steps: number[] = [];
|
|
67
|
+
for (let offset = 0; offset < bytes.length; offset += PF.PATH_STEP_SIZE) {
|
|
68
|
+
steps.push(readU16(bytes, offset));
|
|
69
|
+
}
|
|
70
|
+
return steps;
|
|
71
|
+
}
|
package/src/policy-builder.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { bytesToHex } from "./bytes";
|
|
2
|
-
import { Scope, TypeCode,
|
|
2
|
+
import { Scope, TypeCode, MAX_CONTEXT_PROPERTY_ID } from "./constants";
|
|
3
3
|
import { ConstraintBuilder } from "./constraint";
|
|
4
4
|
import { Descriptor } from "./descriptor";
|
|
5
5
|
import { DescriptorCoder } from "./descriptor-coder";
|
|
6
6
|
import { CallciumError, ValidationError } from "./errors";
|
|
7
|
-
import {
|
|
7
|
+
import { isQuantifier, parsePathSteps } from "./path";
|
|
8
|
+
import { PolicyCoder } from "./policy-coder";
|
|
8
9
|
import { PolicyValidator } from "./policy-validator";
|
|
9
10
|
import { SignatureParser } from "./signature";
|
|
10
11
|
|
|
@@ -29,13 +30,13 @@ type PolicyDraft = {
|
|
|
29
30
|
/** Validate a context-scope path. */
|
|
30
31
|
function validateContextPath(steps: number[]): void {
|
|
31
32
|
if (steps.length !== 1) {
|
|
32
|
-
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step
|
|
33
|
+
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step");
|
|
33
34
|
}
|
|
34
35
|
const step = steps[0]!;
|
|
35
36
|
if (step > MAX_CONTEXT_PROPERTY_ID) {
|
|
36
37
|
throw new CallciumError(
|
|
37
38
|
"UNKNOWN_CONTEXT_PROPERTY",
|
|
38
|
-
`Unknown context property ID 0x${step.toString(16).padStart(4, "0")}
|
|
39
|
+
`Unknown context property ID 0x${step.toString(16).padStart(4, "0")}`,
|
|
39
40
|
);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -47,7 +48,7 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
47
48
|
if (argIndex >= paramCount) {
|
|
48
49
|
throw new CallciumError(
|
|
49
50
|
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
50
|
-
`Param index ${argIndex} out of range (descriptor has ${paramCount} params)
|
|
51
|
+
`Param index ${argIndex} out of range (descriptor has ${paramCount} params)`,
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -61,10 +62,10 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
61
62
|
|
|
62
63
|
if (isQuantifier(step)) {
|
|
63
64
|
if (!isArray) {
|
|
64
|
-
throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node
|
|
65
|
+
throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node");
|
|
65
66
|
}
|
|
66
67
|
if (hasQuantifier) {
|
|
67
|
-
throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed
|
|
68
|
+
throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed");
|
|
68
69
|
}
|
|
69
70
|
hasQuantifier = true;
|
|
70
71
|
}
|
|
@@ -74,7 +75,7 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
74
75
|
if (step >= fieldCount) {
|
|
75
76
|
throw new CallciumError(
|
|
76
77
|
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
77
|
-
`Tuple field index ${step} out of range (tuple has ${fieldCount} fields)
|
|
78
|
+
`Tuple field index ${step} out of range (tuple has ${fieldCount} fields)`,
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
81
|
offset = Descriptor.tupleFieldOffset(desc, offset, step);
|
|
@@ -84,13 +85,13 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
|
84
85
|
if (step >= arrayLength) {
|
|
85
86
|
throw new CallciumError(
|
|
86
87
|
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
87
|
-
`Array index ${step} out of range (static array has ${arrayLength} elements)
|
|
88
|
+
`Array index ${step} out of range (static array has ${arrayLength} elements)`,
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
offset = Descriptor.arrayElementOffset(offset);
|
|
92
93
|
} else {
|
|
93
|
-
throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type
|
|
94
|
+
throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type");
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
}
|
|
@@ -150,13 +151,13 @@ export class PolicyBuilder {
|
|
|
150
151
|
};
|
|
151
152
|
|
|
152
153
|
if (c.operators.length === 0) {
|
|
153
|
-
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator
|
|
154
|
+
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator");
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
// Path shape is established before the scope decides how to navigate it.
|
|
157
158
|
const steps = parsePathSteps(c.path);
|
|
158
159
|
if (steps.length === 0) {
|
|
159
|
-
throw new CallciumError("EMPTY_PATH", "Path must have at least one step
|
|
160
|
+
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
if (c.scope === Scope.CONTEXT) {
|
|
@@ -164,13 +165,13 @@ export class PolicyBuilder {
|
|
|
164
165
|
} else if (c.scope === Scope.CALLDATA) {
|
|
165
166
|
validateCalldataPath(steps, this.draft.descriptor);
|
|
166
167
|
} else {
|
|
167
|
-
throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}
|
|
168
|
+
throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}`);
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
const key = `${c.scope}:${c.path.toLowerCase()}`;
|
|
171
172
|
const currentHashes = this.draft.pathHashes[this.draft.pathHashes.length - 1]!;
|
|
172
173
|
if (currentHashes.has(key)) {
|
|
173
|
-
throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${c.path} in the same group
|
|
174
|
+
throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${c.path} in the same group`);
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
currentHashes.add(key);
|
|
@@ -182,7 +183,7 @@ export class PolicyBuilder {
|
|
|
182
183
|
or(): this {
|
|
183
184
|
const lastGroup = this.draft.groups[this.draft.groups.length - 1]!;
|
|
184
185
|
if (lastGroup.length === 0) {
|
|
185
|
-
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty
|
|
186
|
+
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty");
|
|
186
187
|
}
|
|
187
188
|
this.draft.groups.push([]);
|
|
188
189
|
this.draft.pathHashes.push(new Set());
|
|
@@ -233,7 +234,7 @@ export class PolicyBuilder {
|
|
|
233
234
|
private checkGroups(): void {
|
|
234
235
|
for (let i = 0; i < this.draft.groups.length; i++) {
|
|
235
236
|
if (this.draft.groups[i]!.length === 0) {
|
|
236
|
-
throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty
|
|
237
|
+
throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty`);
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
}
|
package/src/policy-coder.ts
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2
2
|
|
|
3
3
|
import { bytesToHex, hexToBytes, toHex, readU16, readU32, writeBE16, writeBE32 } from "./bytes";
|
|
4
|
-
import {
|
|
5
|
-
DescriptorFormat as DF,
|
|
6
|
-
PolicyFormat as PF,
|
|
7
|
-
Scope,
|
|
8
|
-
Limits,
|
|
9
|
-
Op,
|
|
10
|
-
TypeCode,
|
|
11
|
-
isValidOperatorData,
|
|
12
|
-
isAddressableTarget,
|
|
13
|
-
MAX_CONTEXT_PROPERTY_ID,
|
|
14
|
-
} from "./constants";
|
|
4
|
+
import { DescriptorFormat as DF, PolicyFormat as PF, Scope, Op, TypeCode, MAX_CONTEXT_PROPERTY_ID } from "./constants";
|
|
15
5
|
import { Descriptor } from "./descriptor";
|
|
16
6
|
import { decodeDescriptor } from "./descriptor-coder";
|
|
17
7
|
import { CallciumError } from "./errors";
|
|
18
|
-
import { isLengthOp, isLengthValidType } from "./operators";
|
|
8
|
+
import { isLengthOp, isLengthValidType, toBigInt, isAddressableTarget, isValidOperatorData } from "./operators";
|
|
9
|
+
import { parsePathSteps } from "./path";
|
|
19
10
|
|
|
20
11
|
import type {
|
|
21
12
|
Constraint,
|
|
@@ -28,27 +19,6 @@ import type {
|
|
|
28
19
|
PolicyData,
|
|
29
20
|
} from "./types";
|
|
30
21
|
|
|
31
|
-
///////////////////////////////////////////////////////////////////////////
|
|
32
|
-
// Path helper
|
|
33
|
-
///////////////////////////////////////////////////////////////////////////
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Parse a BE16-encoded hex path into an array of step values.
|
|
37
|
-
* @param path - 0x-prefixed hex string containing BE16-encoded path steps.
|
|
38
|
-
* @returns Array of numeric step values.
|
|
39
|
-
*/
|
|
40
|
-
export function parsePathSteps(path: Hex): number[] {
|
|
41
|
-
const bytes = hexToBytes(path);
|
|
42
|
-
if (bytes.length % PF.PATH_STEP_SIZE !== 0) {
|
|
43
|
-
throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
|
|
44
|
-
}
|
|
45
|
-
const steps: number[] = [];
|
|
46
|
-
for (let offset = 0; offset < bytes.length; offset += PF.PATH_STEP_SIZE) {
|
|
47
|
-
steps.push(readU16(bytes, offset));
|
|
48
|
-
}
|
|
49
|
-
return steps;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
22
|
///////////////////////////////////////////////////////////////////////////
|
|
53
23
|
// Policy field helper
|
|
54
24
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -321,14 +291,25 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
321
291
|
}
|
|
322
292
|
}
|
|
323
293
|
|
|
294
|
+
// An EQ_CTX operand word must be a defined context property ID.
|
|
295
|
+
if (opBase === Op.EQ_CTX) {
|
|
296
|
+
if (toBigInt(data, dataStart) > BigInt(MAX_CONTEXT_PROPERTY_ID)) {
|
|
297
|
+
throw new CallciumError(
|
|
298
|
+
"UNKNOWN_CONTEXT_PROPERTY",
|
|
299
|
+
"EQ_CTX operand references an undefined context property",
|
|
300
|
+
ruleOffset,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
324
305
|
// The path must be non-empty and within the depth cap.
|
|
325
306
|
if (depthValue === 0) {
|
|
326
307
|
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
|
|
327
308
|
}
|
|
328
|
-
if (depthValue >
|
|
309
|
+
if (depthValue > PF.MAX_PATH_DEPTH) {
|
|
329
310
|
throw new CallciumError(
|
|
330
311
|
"PATH_TOO_DEEP",
|
|
331
|
-
`Path depth ${depthValue} exceeds maximum ${
|
|
312
|
+
`Path depth ${depthValue} exceeds maximum ${PF.MAX_PATH_DEPTH}`,
|
|
332
313
|
ruleOffset,
|
|
333
314
|
);
|
|
334
315
|
}
|
|
@@ -419,7 +400,7 @@ function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
|
|
|
419
400
|
const operators = constraint.operators.map(hexToBytes);
|
|
420
401
|
for (const operator of operators) {
|
|
421
402
|
if (operator.length < 1) {
|
|
422
|
-
throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)
|
|
403
|
+
throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
|
|
423
404
|
}
|
|
424
405
|
}
|
|
425
406
|
|
|
@@ -427,14 +408,14 @@ function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
|
|
|
427
408
|
|
|
428
409
|
// Path shape checks precede compilation: the compiler assumes a framed, depth-bounded path.
|
|
429
410
|
if (path.length === 0) {
|
|
430
|
-
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step
|
|
411
|
+
throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step");
|
|
431
412
|
}
|
|
432
413
|
if ((path.length & 1) !== 0) {
|
|
433
|
-
throw new CallciumError("MALFORMED_PATH", "Path byte length must be even
|
|
414
|
+
throw new CallciumError("MALFORMED_PATH", "Path byte length must be even");
|
|
434
415
|
}
|
|
435
416
|
const depth = path.length / 2;
|
|
436
|
-
if (depth >
|
|
437
|
-
throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${
|
|
417
|
+
if (depth > PF.MAX_PATH_DEPTH) {
|
|
418
|
+
throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${PF.MAX_PATH_DEPTH}`);
|
|
438
419
|
}
|
|
439
420
|
|
|
440
421
|
const hint =
|
|
@@ -473,7 +454,7 @@ function sortRules(rules: Rule[]): void {
|
|
|
473
454
|
function encodeRule(rule: Rule): Uint8Array {
|
|
474
455
|
const depth = rule.path.length / 2;
|
|
475
456
|
if (rule.scope === Scope.CONTEXT && depth !== 1) {
|
|
476
|
-
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step
|
|
457
|
+
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step");
|
|
477
458
|
}
|
|
478
459
|
const data = rule.operator.subarray(1);
|
|
479
460
|
const ruleSize = PF.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
|