Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions services/agent/src/mandate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,56 @@ test("bidFromAppraisal clamps to mandate maxBid", () => {
assert.equal(escrow, usdcToStroops(40));
});

test("createSessionMandate rejects unsafe numeric fields", () => {
const p = baseParams();
assert.throws(
() => createSessionMandate({ ...p, basePriceUsdc: Number.NaN }),
(error: unknown) => error instanceof MandateError && error.message === "invalid mandate basePriceUsdc",
);
assert.throws(
() => createSessionMandate({ ...p, commitDeadline: p.commitDeadline + 0.5 }),
(error: unknown) => error instanceof MandateError && error.message === "invalid mandate commitDeadline",
);
assert.throws(
() => createSessionMandate({ ...p, roundId: 1.5 }),
(error: unknown) => error instanceof MandateError && error.message === "invalid mandate roundId",
);
});

test("verifySessionMandate rejects a signed mandate with unsafe fields", () => {
const p = baseParams();
const { mandate } = createSessionMandate(p);
const { signature: _signature, ...payload } = mandate;
const malformed = {
...payload,
basePriceUsdc: Number.MAX_SAFE_INTEGER + 1,
signature: Keypair.fromSecret(p.principalSecret)
.sign(mandateDigest({ ...payload, basePriceUsdc: Number.MAX_SAFE_INTEGER + 1 }))
.toString("base64"),
};
assert.throws(
() => verifySessionMandate(malformed, { clock: p.clock }),
(error: unknown) => error instanceof MandateError && error.message === "invalid mandate basePriceUsdc",
);
});

test("verifySessionMandate rejects malformed stroop strings", () => {
const p = baseParams();
const { mandate } = createSessionMandate(p);
const { signature: _signature, ...payload } = mandate;
const malformedPayload = { ...payload, maxBidStroops: "1.5" };
const malformed = {
...malformedPayload,
signature: Keypair.fromSecret(p.principalSecret)
.sign(mandateDigest(malformedPayload))
.toString("base64"),
};
assert.throws(
() => verifySessionMandate(malformed, { clock: p.clock }),
(error: unknown) => error instanceof MandateError && error.message === "invalid mandate maxBidStroops",
);
});

test("verifySessionMandate rejects when issuedAt > expiresAt", () => {
const p = baseParams();
const { mandate } = createSessionMandate(p);
Expand Down
28 changes: 28 additions & 0 deletions services/agent/src/mandate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,32 @@ export interface SessionMandate extends SessionMandatePayload {
export class MandateError extends Error {}
export class MandateCapError extends MandateError {}

function invalidNumericField(field: string): never {
throw new MandateError(`invalid mandate ${field}`);
}

function assertSafeMandateInteger(value: number, field: string, minimum = 0): void {
if (!Number.isSafeInteger(value) || value < minimum) invalidNumericField(field);
}

function assertMandateIntegerString(value: string, field: string, minimum = 0n): void {
if (!/^(0|[1-9]\d*)$/.test(value)) invalidNumericField(field);
const parsed = BigInt(value);
if (parsed < minimum) invalidNumericField(field);
}

function validateMandateNumbers(payload: SessionMandatePayload): void {
assertSafeMandateInteger(payload.basePriceUsdc, "basePriceUsdc");
assertSafeMandateInteger(payload.commitDeadline, "commitDeadline");
assertSafeMandateInteger(payload.issuedAt, "issuedAt");
assertSafeMandateInteger(payload.expiresAt, "expiresAt");
assertMandateIntegerString(payload.roundId, "roundId", 1n);
assertMandateIntegerString(payload.maxBidStroops, "maxBidStroops");
assertMandateIntegerString(payload.maxEscrowStroops, "maxEscrowStroops");
assertMandateIntegerString(payload.maxAppraisalSpendStroops, "maxAppraisalSpendStroops");
assertMandateIntegerString(payload.appraisalPriceStroops, "appraisalPriceStroops");
}

function validateMandateTimestampOrdering(payload: SessionMandatePayload): void {
if (payload.issuedAt > payload.expiresAt) {
throw new MandateError("issuedAt must be <= expiresAt");
Expand Down Expand Up @@ -134,6 +160,7 @@ export function createSessionMandate(params: CreateMandateParams): {
issuedAt: now,
expiresAt: now + (params.ttlSeconds ?? 3600),
};
validateMandateNumbers(payload);
validateMandateTimestampOrdering(payload);
const sig = principal.sign(mandateDigest(payload));
return {
Expand All @@ -158,6 +185,7 @@ export function verifySessionMandate(
);
if (!ok) throw new MandateError("invalid mandate signature");

validateMandateNumbers(payload);
validateMandateTimestampOrdering(payload);

const now = opts?.now ?? (opts?.clock ?? systemClock).nowSeconds();
Expand Down
Loading