@callcium/sdk 0.10.0 → 0.11.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 +150 -103
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +683 -579
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/calldata-reader.ts +14 -1
- package/src/constants.ts +54 -11
- package/src/constraint.ts +131 -69
- package/src/descriptor-coder.ts +37 -66
- package/src/descriptor.ts +54 -26
- package/src/errors.ts +12 -2
- package/src/index.ts +1 -0
- package/src/operators.ts +36 -5
- package/src/path.ts +15 -6
- package/src/policy-builder.ts +94 -32
- package/src/policy-coder.ts +100 -45
- package/src/policy-enforcer.ts +85 -103
- package/src/policy-validator.ts +52 -35
- package/src/signature.ts +21 -19
- package/src/type-desc.ts +6 -6
package/src/policy-coder.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
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 {
|
|
4
|
+
import {
|
|
5
|
+
DescriptorFormat as DF,
|
|
6
|
+
fieldMax,
|
|
7
|
+
formatCode,
|
|
8
|
+
MAX_CONTEXT_PROPERTY_ID,
|
|
9
|
+
Op,
|
|
10
|
+
PolicyFormat as PF,
|
|
11
|
+
Scope,
|
|
12
|
+
TypeCode,
|
|
13
|
+
} from "./constants";
|
|
5
14
|
import { Descriptor } from "./descriptor";
|
|
6
15
|
import { decodeDescriptor } from "./descriptor-coder";
|
|
7
|
-
import { CallciumError } from "./errors";
|
|
8
|
-
import {
|
|
16
|
+
import { CallciumError, type CallciumErrorCode } from "./errors";
|
|
17
|
+
import {
|
|
18
|
+
isLengthOp,
|
|
19
|
+
isLengthValidType,
|
|
20
|
+
toBigInt,
|
|
21
|
+
isAddressableTarget,
|
|
22
|
+
isValidOperatorData,
|
|
23
|
+
toOperatorBytes,
|
|
24
|
+
} from "./operators";
|
|
9
25
|
import { parsePathSteps } from "./path";
|
|
10
26
|
|
|
11
27
|
import type {
|
|
@@ -28,6 +44,24 @@ function field<T>(value: T, start: number, end: number): Field<T> {
|
|
|
28
44
|
return { value, span: { start, end } };
|
|
29
45
|
}
|
|
30
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Wrap the hex rendering of `data[start, end)` with its span, rendering on first read.
|
|
49
|
+
* Enforcement reads the span as bytes, so the string is built only for a caller that asks.
|
|
50
|
+
* The field reads and assigns like the plain one it replaces.
|
|
51
|
+
*/
|
|
52
|
+
function hexField(data: Uint8Array, start: number, end: number): Field<Hex> {
|
|
53
|
+
let hex: Hex | undefined;
|
|
54
|
+
return {
|
|
55
|
+
get value(): Hex {
|
|
56
|
+
return (hex ??= toHex(data, start, end));
|
|
57
|
+
},
|
|
58
|
+
set value(replacement: Hex) {
|
|
59
|
+
hex = replacement;
|
|
60
|
+
},
|
|
61
|
+
span: { start, end },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
///////////////////////////////////////////////////////////////////////////
|
|
32
66
|
// Hint block
|
|
33
67
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -123,7 +157,7 @@ function hintTargetTypeCode(data: Uint8Array, hintStart: number, hintSize: numbe
|
|
|
123
157
|
/** Decode a policy blob, returning the structural representation and the blob bytes it spans. */
|
|
124
158
|
export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Array } {
|
|
125
159
|
const data = hexToBytes(blob);
|
|
126
|
-
if (data.length < PF.DESC_OFFSET
|
|
160
|
+
if (data.length < PF.DESC_OFFSET) {
|
|
127
161
|
throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
|
|
128
162
|
}
|
|
129
163
|
|
|
@@ -169,7 +203,8 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
169
203
|
end: descStart + param.span.end,
|
|
170
204
|
},
|
|
171
205
|
}));
|
|
172
|
-
|
|
206
|
+
|
|
207
|
+
const descriptorRaw = hexField(data, descStart, descEnd);
|
|
173
208
|
|
|
174
209
|
const groupCountStart = groupCountOffset;
|
|
175
210
|
const groupCountEnd = groupCountStart + PF.GROUP_COUNT_SIZE;
|
|
@@ -191,6 +226,10 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
191
226
|
const groupBodyStart = offset + PF.GROUP_HEADER_SIZE;
|
|
192
227
|
const groupEnd = groupBodyStart + groupSizeValue;
|
|
193
228
|
|
|
229
|
+
if (groupEnd > data.length) {
|
|
230
|
+
throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
231
|
+
}
|
|
232
|
+
|
|
194
233
|
const ruleCountStart = offset;
|
|
195
234
|
const ruleCountValue = readU16(data, ruleCountStart);
|
|
196
235
|
if (ruleCountValue === 0) {
|
|
@@ -199,9 +238,6 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
199
238
|
if (groupSizeValue < ruleCountValue * PF.RULE_MIN_SIZE) {
|
|
200
239
|
throw new CallciumError("GROUP_TOO_SMALL", "Declared group size is too small for its rule count", offset);
|
|
201
240
|
}
|
|
202
|
-
if (groupEnd > data.length) {
|
|
203
|
-
throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
|
|
204
|
-
}
|
|
205
241
|
|
|
206
242
|
const rules: DecodedRule[] = [];
|
|
207
243
|
let ruleOffset = groupBodyStart;
|
|
@@ -219,8 +255,8 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
219
255
|
ruleOffset,
|
|
220
256
|
);
|
|
221
257
|
}
|
|
222
|
-
if (ruleOffset + ruleSizeValue >
|
|
223
|
-
throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond
|
|
258
|
+
if (ruleOffset + ruleSizeValue > data.length) {
|
|
259
|
+
throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond policy blob", ruleOffset);
|
|
224
260
|
}
|
|
225
261
|
|
|
226
262
|
const scopeOffset = ruleOffset + PF.RULE_SCOPE_OFFSET;
|
|
@@ -282,12 +318,13 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
282
318
|
// IN operands must be strictly ascending (unsigned); strictness also rejects duplicates.
|
|
283
319
|
if (opBase === Op.IN) {
|
|
284
320
|
const wordCount = dataLengthValue / 32;
|
|
321
|
+
let prev = toBigInt(data, dataStart);
|
|
285
322
|
for (let word = 1; word < wordCount; word++) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
if (compareBytes(prev, cur) >= 0) {
|
|
323
|
+
const cur = toBigInt(data, dataStart + word * 32);
|
|
324
|
+
if (cur <= prev) {
|
|
289
325
|
throw new CallciumError("UNSORTED_IN_SET", "IN operands must be strictly ascending", ruleOffset);
|
|
290
326
|
}
|
|
327
|
+
prev = cur;
|
|
291
328
|
}
|
|
292
329
|
}
|
|
293
330
|
|
|
@@ -327,7 +364,7 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
327
364
|
if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) {
|
|
328
365
|
throw new CallciumError(
|
|
329
366
|
"UNKNOWN_CONTEXT_PROPERTY",
|
|
330
|
-
`Context rule references undefined context property
|
|
367
|
+
`Context rule references undefined context property ${formatCode(contextPropertyId, PF.PATH_STEP_SIZE)}`,
|
|
331
368
|
ruleOffset,
|
|
332
369
|
);
|
|
333
370
|
}
|
|
@@ -340,14 +377,17 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
340
377
|
pathDepth: field(depthValue, depthOffset, depthOffset + 1),
|
|
341
378
|
path: field(pathHex, pathStart, pathStart + pathLength),
|
|
342
379
|
...(hintSize > 0 && {
|
|
343
|
-
hint:
|
|
380
|
+
hint: hexField(data, hintStart, hintStart + hintSize),
|
|
344
381
|
}),
|
|
345
382
|
opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PF.RULE_OPCODE_SIZE),
|
|
346
383
|
dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PF.RULE_DATALENGTH_SIZE),
|
|
347
|
-
data:
|
|
384
|
+
data: hexField(data, dataStart, dataStart + dataLengthValue),
|
|
348
385
|
span: { start: ruleOffset, end: ruleOffset + ruleSizeValue },
|
|
349
386
|
});
|
|
350
387
|
|
|
388
|
+
if (ruleOffset + ruleSizeValue > groupEnd) {
|
|
389
|
+
throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
|
|
390
|
+
}
|
|
351
391
|
ruleOffset += ruleSizeValue;
|
|
352
392
|
}
|
|
353
393
|
|
|
@@ -374,7 +414,12 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
374
414
|
selector: field(selectorHex, selectorStart, selectorEnd),
|
|
375
415
|
descLength: field(descLengthValue, descLengthStart, descLengthStart + PF.DESC_LENGTH_SIZE),
|
|
376
416
|
descriptor: {
|
|
377
|
-
raw:
|
|
417
|
+
get raw(): Hex {
|
|
418
|
+
return descriptorRaw.value;
|
|
419
|
+
},
|
|
420
|
+
set raw(replacement: Hex) {
|
|
421
|
+
descriptorRaw.value = replacement;
|
|
422
|
+
},
|
|
378
423
|
header: field({ version: desc.version, paramCount: desc.params.length }, descStart, descStart + DF.HEADER_SIZE),
|
|
379
424
|
params,
|
|
380
425
|
span: { start: descStart, end: descEnd },
|
|
@@ -395,15 +440,11 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
|
|
|
395
440
|
|
|
396
441
|
type Rule = { scope: number; path: Uint8Array; operator: Uint8Array; hint: Uint8Array };
|
|
397
442
|
|
|
398
|
-
/**
|
|
399
|
-
|
|
400
|
-
const operators = constraint.operators.map(hexToBytes);
|
|
401
|
-
for (const operator of operators) {
|
|
402
|
-
if (operator.length < 1) {
|
|
403
|
-
throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
|
|
404
|
-
}
|
|
405
|
-
}
|
|
443
|
+
/** The path bytes and compiled hint every rule a constraint produces carries. */
|
|
444
|
+
type ConstraintTarget = { path: Uint8Array; hint: Uint8Array };
|
|
406
445
|
|
|
446
|
+
/** Read a constraint's path and compile the hint its rules carry. */
|
|
447
|
+
function readConstraintTarget(constraint: Constraint, desc: Uint8Array): ConstraintTarget {
|
|
407
448
|
const path = hexToBytes(constraint.path);
|
|
408
449
|
|
|
409
450
|
// Path shape checks precede compilation: the compiler assumes a framed, depth-bounded path.
|
|
@@ -422,7 +463,21 @@ function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
|
|
|
422
463
|
constraint.scope === Scope.CALLDATA
|
|
423
464
|
? Descriptor.compileHint(desc, parsePathSteps(constraint.path))
|
|
424
465
|
: new Uint8Array(0);
|
|
425
|
-
return
|
|
466
|
+
return { path, hint };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Flatten a Constraint into one Rule per operator, compiling its hint against the descriptor. */
|
|
470
|
+
function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
|
|
471
|
+
// The rule is the unit these checks run over: each rule's operator is established before the
|
|
472
|
+
// path and hint it shares with its siblings. A constraint carrying no operator produces no rule,
|
|
473
|
+
// so its path is never read.
|
|
474
|
+
let target: ConstraintTarget | undefined;
|
|
475
|
+
|
|
476
|
+
return constraint.operators.map((operatorHex) => {
|
|
477
|
+
const operator = toOperatorBytes(operatorHex);
|
|
478
|
+
target ??= readConstraintTarget(constraint, desc);
|
|
479
|
+
return { scope: constraint.scope, operator, ...target };
|
|
480
|
+
});
|
|
426
481
|
}
|
|
427
482
|
|
|
428
483
|
/** Compare two byte arrays lexicographically. */
|
|
@@ -447,6 +502,12 @@ function sortRules(rules: Rule[]): void {
|
|
|
447
502
|
});
|
|
448
503
|
}
|
|
449
504
|
|
|
505
|
+
/** Require a value to fit the big-endian field of `byteWidth` bytes. */
|
|
506
|
+
function requireFieldFits(value: number, byteWidth: number, code: CallciumErrorCode, subject: string): void {
|
|
507
|
+
const max = fieldMax(byteWidth);
|
|
508
|
+
if (value > max) throw new CallciumError(code, `${subject} ${value} exceeds maximum ${max}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
450
511
|
/**
|
|
451
512
|
* Serialize a single rule to its wire format bytes.
|
|
452
513
|
* Path shape and operator framing are established by `flattenConstraint`.
|
|
@@ -458,9 +519,7 @@ function encodeRule(rule: Rule): Uint8Array {
|
|
|
458
519
|
}
|
|
459
520
|
const data = rule.operator.subarray(1);
|
|
460
521
|
const ruleSize = PF.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
|
|
461
|
-
|
|
462
|
-
throw new CallciumError("RULE_SIZE_OVERFLOW", `Rule size ${ruleSize} exceeds maximum 65535`);
|
|
463
|
-
}
|
|
522
|
+
requireFieldFits(ruleSize, PF.RULE_SIZE_SIZE, "RULE_SIZE_OVERFLOW", "Rule size");
|
|
464
523
|
|
|
465
524
|
const buf = new Uint8Array(ruleSize);
|
|
466
525
|
writeBE16(buf, 0, ruleSize);
|
|
@@ -511,9 +570,7 @@ function encode(data: PolicyData): Hex {
|
|
|
511
570
|
if (data.groups.length === 0) {
|
|
512
571
|
throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
|
|
513
572
|
}
|
|
514
|
-
|
|
515
|
-
throw new CallciumError("GROUP_COUNT_OVERFLOW", `Group count ${data.groups.length} exceeds maximum 255`);
|
|
516
|
-
}
|
|
573
|
+
requireFieldFits(data.groups.length, PF.GROUP_COUNT_SIZE, "GROUP_COUNT_OVERFLOW", "Group count");
|
|
517
574
|
|
|
518
575
|
// Flatten constraints into rules and sort within each group. Hints are compiled first because
|
|
519
576
|
// they are part of the rule bytes the group hash covers.
|
|
@@ -523,20 +580,13 @@ function encode(data: PolicyData): Hex {
|
|
|
523
580
|
return rules;
|
|
524
581
|
});
|
|
525
582
|
|
|
526
|
-
|
|
527
|
-
throw new CallciumError("DESC_LENGTH_OVERFLOW", `Descriptor length ${descBytes.length} exceeds maximum 65535`);
|
|
528
|
-
}
|
|
583
|
+
requireFieldFits(descBytes.length, PF.DESC_LENGTH_SIZE, "DESC_LENGTH_OVERFLOW", "Descriptor length");
|
|
529
584
|
|
|
530
585
|
const encodedGroups = sortedGroups.map((rules, groupIndex) => {
|
|
531
586
|
if (rules.length === 0) {
|
|
532
587
|
throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
|
|
533
588
|
}
|
|
534
|
-
|
|
535
|
-
throw new CallciumError(
|
|
536
|
-
"RULE_COUNT_OVERFLOW",
|
|
537
|
-
`Group ${groupIndex} rule count ${rules.length} exceeds maximum 65535`,
|
|
538
|
-
);
|
|
539
|
-
}
|
|
589
|
+
requireFieldFits(rules.length, PF.GROUP_RULECOUNT_SIZE, "RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count`);
|
|
540
590
|
return { wireBytes: encodeGroupRules(rules), ruleCount: rules.length };
|
|
541
591
|
});
|
|
542
592
|
|
|
@@ -547,7 +597,13 @@ function encode(data: PolicyData): Hex {
|
|
|
547
597
|
encodedGroups.sort((a, b) => compareBytes(hashes.get(a)!, hashes.get(b)!));
|
|
548
598
|
}
|
|
549
599
|
|
|
550
|
-
const selectorBytes = data.isSelectorless ? new Uint8Array(
|
|
600
|
+
const selectorBytes = data.isSelectorless ? new Uint8Array(PF.SELECTOR_SIZE) : hexToBytes(data.selector);
|
|
601
|
+
if (selectorBytes.length !== PF.SELECTOR_SIZE) {
|
|
602
|
+
throw new CallciumError(
|
|
603
|
+
"MALFORMED_SELECTOR",
|
|
604
|
+
`Selector must be ${PF.SELECTOR_SIZE} bytes, got ${selectorBytes.length}`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
551
607
|
|
|
552
608
|
const headerByte = PF.VERSION | (data.isSelectorless ? PF.FLAG_NO_SELECTOR : 0);
|
|
553
609
|
|
|
@@ -599,8 +655,8 @@ function decode(blob: Hex): PolicyData {
|
|
|
599
655
|
const { policy } = decodePolicy(blob);
|
|
600
656
|
|
|
601
657
|
const groups: Constraint[][] = policy.groups.map((group) => {
|
|
658
|
+
// A Map keeps its insertion order, which is the order the rules named their constraints in.
|
|
602
659
|
const constraintMap = new Map<string, Constraint>();
|
|
603
|
-
const constraintOrder: string[] = [];
|
|
604
660
|
|
|
605
661
|
for (const rule of group.rules) {
|
|
606
662
|
const hintHex = rule.hint?.value;
|
|
@@ -619,11 +675,10 @@ function decode(blob: Hex): PolicyData {
|
|
|
619
675
|
span: rule.span,
|
|
620
676
|
};
|
|
621
677
|
constraintMap.set(key, constraint);
|
|
622
|
-
constraintOrder.push(key);
|
|
623
678
|
}
|
|
624
679
|
}
|
|
625
680
|
|
|
626
|
-
return
|
|
681
|
+
return [...constraintMap.values()];
|
|
627
682
|
});
|
|
628
683
|
|
|
629
684
|
return {
|