Harden contract helper validators

This commit is contained in:
jb
2026-09-19 10:07:35 -04:00
parent 614bbf9290
commit 373eb60d0b
3 changed files with 84 additions and 8 deletions
+66 -6
View File
@@ -11,6 +11,7 @@ import type {
DatasetSyncState,
InventoryCursorLookup,
InventoryCursorState,
InventoryQueryResult,
MissingRequirement,
Requirement,
ResourceAuthorization,
@@ -83,11 +84,14 @@ const SYSTEM_JOB_DATASETS: Readonly<Record<'reference' | 'public_feed' | 'mainte
export type InventoryCursorValidation =
| { valid: true }
| { valid: false; reason: 'token_mismatch' | 'expired' | 'exhausted' | 'caller_mismatch' | 'query_mismatch' | 'resource_mismatch' | 'authorization_mismatch' | 'input_version_mismatch' };
| { valid: false; reason: 'token_mismatch' | 'expired' | 'exhausted' | 'caller_mismatch' | 'query_mismatch' | 'resource_mismatch' | 'authorization_mismatch' | 'input_version_mismatch' | 'malformed_state' };
export type InventoryPageValidation =
| { valid: true }
| { valid: false; reason: 'invalid_page_state' | 'terminal_cursor' | 'empty_page_summary' };
export type CompleteReportPinValidation =
| { valid: true }
| { valid: false; reason: 'missing_pin' | 'unexpected_pin' | 'duplicate_pin' | 'principal_mismatch' | 'revision_mismatch' | 'incomplete_coverage' };
| { valid: false; reason: 'missing_pin' | 'unexpected_pin' | 'duplicate_pin' | 'duplicate_resource' | 'principal_mismatch' | 'revision_mismatch' | 'incomplete_coverage' };
export type DatasetJobFenceValidation =
| { valid: true }
@@ -107,6 +111,7 @@ export type CapabilityScopeValidation = {
preservedScopes: readonly string[];
missingScopes: readonly string[];
unexpectedScopes: readonly string[];
duplicateScopes: readonly string[];
};
/**
@@ -128,18 +133,20 @@ function validateScopeLists(
approvedExistingScopes: readonly string[],
): CapabilityScopeValidation {
const serverAllowlist = new Set(SERVER_SCOPE_ALLOWLIST);
const duplicateScopes = [...new Set([...findDuplicates(requestedScopes), ...findDuplicates(approvedExistingScopes)])];
const preservedScopes = approvedExistingScopes.filter((scope) => serverAllowlist.has(scope));
const allowlistedScopes = [...new Set([...requiredScopes, ...preservedScopes])];
const allowlisted = new Set(allowlistedScopes);
const missingScopes = requiredScopes.filter((scope) => !requestedScopes.includes(scope));
const unexpectedScopes = requestedScopes.filter((scope) => !allowlisted.has(scope) || !serverAllowlist.has(scope));
return {
valid: missingScopes.length === 0 && unexpectedScopes.length === 0,
valid: missingScopes.length === 0 && unexpectedScopes.length === 0 && duplicateScopes.length === 0,
requiredScopes: [...requiredScopes],
allowlistedScopes,
preservedScopes: preservedScopes.filter((scope) => requestedScopes.includes(scope)),
missingScopes,
unexpectedScopes,
duplicateScopes,
};
}
@@ -160,6 +167,7 @@ export function validateConsentCallback(binding: ConsentBinding, callback: Conse
if (binding.state !== 'pending' || !Number.isFinite(now) || !Number.isFinite(binding.createdAt) || !Number.isFinite(binding.expiresAt)
|| binding.expiresAt <= binding.createdAt || now < binding.createdAt || now >= binding.expiresAt) return { valid: false, reason: 'binding_mismatch' };
if (callback.resourceGrants.length !== binding.resourceRequests.length) return { valid: false, reason: 'resource_mismatch' };
if (hasDuplicatePrincipals(binding.resourceRequests.map((request) => request.resource.principal))) return { valid: false, reason: 'duplicate_resource' };
const seen = new Set<string>();
for (const grant of callback.resourceGrants) {
@@ -201,20 +209,46 @@ function samePrincipal(left: ResourcePrincipal, right: ResourcePrincipal): boole
}
function principalFingerprint(principal: ResourcePrincipal): string {
return JSON.stringify(principal);
switch (principal.kind) {
case 'user': return `user\u0000${principal.id}`;
case 'character': return `character\u0000${principal.id}\u0000${principal.ownerUserId}`;
case 'corporation': return `corporation\u0000${principal.id}\u0000${principal.ownerUserId}`;
case 'system': return `system\u0000${principal.id}`;
}
}
function findDuplicates(values: readonly string[]): string[] {
const seen = new Set<string>();
const duplicates = new Set<string>();
for (const value of values) {
if (seen.has(value)) duplicates.add(value);
seen.add(value);
}
return [...duplicates];
}
function hasDuplicatePrincipals(principals: readonly ResourcePrincipal[]): boolean {
return findDuplicates(principals.map(principalFingerprint)).length > 0;
}
function sameStrings(left: readonly string[], right: readonly string[]): boolean {
return left.length === right.length && left.every((value) => right.includes(value));
if (left.length !== right.length) return false;
const sortedLeft = [...left].sort();
const sortedRight = [...right].sort();
return sortedLeft.every((value, index) => value === sortedRight[index]);
}
/** Validate every server-resolved value before consuming an opaque cursor. */
export function validateInventoryCursorUse(state: InventoryCursorState, lookup: InventoryCursorLookup): InventoryCursorValidation {
if (state.token !== lookup.token) return { valid: false, reason: 'token_mismatch' };
if (typeof state.token !== 'string' || state.token.length === 0 || state.token !== lookup.token) return { valid: false, reason: 'token_mismatch' };
if (state.status !== 'active' && state.status !== 'exhausted') return { valid: false, reason: 'malformed_state' };
if (!validCursorPosition(state.position)) return { valid: false, reason: 'malformed_state' };
if (state.status === 'exhausted') return { valid: false, reason: 'exhausted' };
if (!Number.isFinite(lookup.now) || !Number.isFinite(state.pin.expiresAt) || lookup.now >= state.pin.expiresAt) return { valid: false, reason: 'expired' };
if (state.pin.callerUserId !== lookup.callerUserId) return { valid: false, reason: 'caller_mismatch' };
if (state.pin.queryFingerprint !== lookup.queryFingerprint) return { valid: false, reason: 'query_mismatch' };
if (hasDuplicatePrincipals(state.pin.selectedResources.map((resource) => resource.principal))
|| hasDuplicatePrincipals(lookup.selectedResources.map((resource) => resource.principal))) return { valid: false, reason: 'resource_mismatch' };
if (!sameSelectedResourceList(state.pin.selectedResources, lookup.selectedResources)) return { valid: false, reason: 'resource_mismatch' };
if (!sameRecord(state.pin.authorizationRevisions, lookup.authorizationRevisions)
|| !sameRecord(state.pin.authorizationEpochs, lookup.authorizationEpochs)) return { valid: false, reason: 'authorization_mismatch' };
@@ -223,6 +257,30 @@ export function validateInventoryCursorUse(state: InventoryCursorState, lookup:
return { valid: true };
}
/** Enforce the page discriminants without implementing page retrieval. */
export function validateInventoryPageResult(result: InventoryQueryResult): InventoryPageValidation {
if (result.status !== 'ready') return { valid: true };
if (result.pageState === 'after_end') {
return result.rows.length === 0 && result.nextCursor === null
? { valid: true }
: { valid: false, reason: 'terminal_cursor' };
}
if (result.pageState === 'initial' && result.rows.length === 0
&& (result.summary.matchingRows !== 0 || result.summary.matchingQuantity !== '0' || (result.summary.estimatedValue !== null && result.summary.estimatedValue !== '0'))) {
return { valid: false, reason: 'empty_page_summary' };
}
if (result.pageState === 'continuation' && result.rows.length === 0) return { valid: false, reason: 'invalid_page_state' };
return { valid: true };
}
function validCursorPosition(position: unknown): boolean {
if (!position || typeof position !== 'object') return false;
const candidate = position as { offset?: unknown; lastItemId?: unknown };
if (!Number.isSafeInteger(candidate.offset) || (candidate.offset as number) < 0) return false;
if (candidate.offset === 0) return candidate.lastItemId === null;
return typeof candidate.lastItemId === 'string' && /^(0|[1-9][0-9]*)$/.test(candidate.lastItemId);
}
function sameSelectedResourceList(left: readonly SelectedResource[], right: readonly SelectedResource[]): boolean {
return left.length === right.length && left.every((resource) => right.some((candidate) => sameSelectedResource([resource], candidate)));
}
@@ -236,6 +294,8 @@ function sameRecord<T extends string | number | null>(left: Readonly<Record<stri
/** Validate the exact selected-resource × required-dataset pin set. */
export function validateCompleteReportPins(input: CompleteReportInputPin): CompleteReportPinValidation {
if (input.pinSet !== 'exact_selected_resource_dataset_set') return { valid: false, reason: 'unexpected_pin' };
if (hasDuplicatePrincipals(input.selectedResources.map((resource) => resource.principal))
|| findDuplicates(input.requiredDatasetKinds).length > 0) return { valid: false, reason: 'duplicate_resource' };
const expected = new Set(input.selectedResources.flatMap((resource) => input.requiredDatasetKinds.map((dataset) => `${principalFingerprint(resource.principal)}|${dataset}`)));
const seen = new Set<string>();
for (const pin of input.datasetPins) {