142 lines
8.9 KiB
TypeScript
142 lines
8.9 KiB
TypeScript
/** Pure poll-journal accounting, not MAC/terminal authority. Full reservations
|
|
* include unresolved work; compact history continues paying bytes and rows.
|
|
* Bytes are logical charges, never a claim about PostgreSQL relation size. */
|
|
const ATTEMPT_BYTES = 8 * 1024 * 1024
|
|
const ADMISSION_BYTES = 32768
|
|
const ATTEMPT_ROWS = 4019 // anchor + 2008 intents + 2008 acks + receipt + quarantine
|
|
const DAY_MS = 86400000
|
|
const DATE_MAX = 8640000000000000
|
|
|
|
export interface PollStoragePolicy {
|
|
attemptBytes: number
|
|
admissionBytes: number
|
|
attemptRows: number
|
|
globalAttempts: number
|
|
ownerAttempts: number
|
|
globalBytes: number
|
|
ownerBytes: number
|
|
globalRows: number
|
|
ownerRows: number
|
|
retentionDays: number
|
|
safetyWindowMs: number
|
|
}
|
|
export const SERVER_POLL_STORAGE_POLICY: Readonly<PollStoragePolicy> = Object.freeze({
|
|
attemptBytes: ATTEMPT_BYTES, admissionBytes: ADMISSION_BYTES, attemptRows: ATTEMPT_ROWS,
|
|
globalAttempts: 128, ownerAttempts: 8,
|
|
globalBytes: 128 * (ATTEMPT_BYTES + ADMISSION_BYTES), ownerBytes: 8 * (ATTEMPT_BYTES + ADMISSION_BYTES),
|
|
globalRows: 128 * ATTEMPT_ROWS, ownerRows: 8 * ATTEMPT_ROWS,
|
|
retentionDays: 14, safetyWindowMs: 60000,
|
|
})
|
|
|
|
export class PollStorageRefused extends Error {
|
|
readonly automaticReplay=false
|
|
constructor() { super("Poll storage state unavailable"); this.name = "PollStorageRefused" }
|
|
}
|
|
function refuse(): never { throw new PollStorageRefused() }
|
|
const integer = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && !Object.is(value,-0)
|
|
const timestamp = (value: unknown): value is number => integer(value) && value <= DATE_MAX
|
|
const record = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
const days = (value: unknown): value is number => integer(value) && value >= 1 && value <= 365
|
|
const uuid = (value: unknown): value is string => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
|
const hash = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value)
|
|
|
|
/** Trusted composition only. Fixed reservations/caps cannot be weakened by a
|
|
* caller; only retention and its safety window are configurable. */
|
|
export function pollStoragePolicy(raw: unknown = SERVER_POLL_STORAGE_POLICY): Readonly<PollStoragePolicy> {
|
|
if (!record(raw) || Object.keys(raw).some(key => !Object.hasOwn(SERVER_POLL_STORAGE_POLICY, key))) refuse()
|
|
for (const key of Object.keys(SERVER_POLL_STORAGE_POLICY) as (keyof PollStoragePolicy)[]) {
|
|
if (key !== "retentionDays" && key !== "safetyWindowMs" && raw[key] !== SERVER_POLL_STORAGE_POLICY[key]) refuse()
|
|
}
|
|
if (!days(raw.retentionDays) || !integer(raw.safetyWindowMs) || raw.safetyWindowMs < 60000 || raw.safetyWindowMs > DAY_MS) refuse()
|
|
return Object.freeze({ ...SERVER_POLL_STORAGE_POLICY, retentionDays: raw.retentionDays, safetyWindowMs: raw.safetyWindowMs })
|
|
}
|
|
|
|
export interface PollStorageScope {
|
|
receiptId: string
|
|
kind: "rest" | "mcp"
|
|
executionId: string
|
|
ownerId: string
|
|
bindingProof: string
|
|
contextProof: string
|
|
issuedAt: number
|
|
expiresAt: number
|
|
}
|
|
/** Expired scope parsing is exclusively for retained metadata, never a claim. */
|
|
export function pollStorageScope(raw: unknown, options: { now: number; allowExpired?: boolean }): Readonly<PollStorageScope> {
|
|
if (!record(raw) || !timestamp(options.now) || options.allowExpired !== undefined && typeof options.allowExpired !== "boolean"
|
|
|| Object.keys(raw).some(key => !["receiptId", "kind", "executionId", "ownerId", "bindingProof", "contextProof", "issuedAt", "expiresAt"].includes(key))
|
|
|| !uuid(raw.receiptId) || !uuid(raw.executionId) || raw.kind !== "rest" && raw.kind !== "mcp"
|
|
|| typeof raw.ownerId !== "string" || !raw.ownerId.length || raw.ownerId.length > 200 || raw.ownerId.trim() !== raw.ownerId || /[\x00-\x1f\x7f-\x9f]/.test(raw.ownerId)
|
|
|| !hash(raw.bindingProof) || !hash(raw.contextProof) || !timestamp(raw.issuedAt) || !timestamp(raw.expiresAt)
|
|
|| raw.expiresAt <= raw.issuedAt || raw.issuedAt > options.now || !options.allowExpired && options.now >= raw.expiresAt) refuse()
|
|
return Object.freeze({ receiptId: raw.receiptId, kind: raw.kind, executionId: raw.executionId, ownerId: raw.ownerId,
|
|
bindingProof: raw.bindingProof, contextProof: raw.contextProof, issuedAt: raw.issuedAt, expiresAt: raw.expiresAt })
|
|
}
|
|
|
|
export interface PollStorageUsage { attempts: number; bytes: number; rows: number; compactedAttempts?: number; archiveBytes?: number }
|
|
function usage(raw: PollStorageUsage) {
|
|
if (!record(raw)) refuse()
|
|
const compact = Object.hasOwn(raw,"compactedAttempts"), archive = Object.hasOwn(raw,"archiveBytes")
|
|
const keys = Object.keys(raw).sort().join(",")
|
|
if (compact !== archive || keys !== (compact ? "archiveBytes,attempts,bytes,compactedAttempts,rows" : "attempts,bytes,rows")) refuse()
|
|
const count = compact ? raw.compactedAttempts : 0, encoded = archive ? raw.archiveBytes : 0
|
|
if (!integer(raw.attempts) || !integer(raw.bytes) || !integer(raw.rows) || !integer(count) || !integer(encoded)
|
|
|| encoded < count * 40 || encoded > count * 131072) refuse()
|
|
const expectedRows = raw.attempts * ATTEMPT_ROWS + count * 2
|
|
const expectedBytes = raw.attempts * (ATTEMPT_BYTES + ADMISSION_BYTES) + count * ADMISSION_BYTES + encoded
|
|
if (!integer(expectedRows) || !integer(expectedBytes) || raw.rows !== expectedRows || raw.bytes !== expectedBytes) refuse()
|
|
return { ...raw, compactedAttempts:count, archiveBytes:encoded }
|
|
}
|
|
/** Available NEW full reservations under every unchanged cap. SQL aggregate
|
|
* equality and all retained charge MACs must be checked by the admission owner;
|
|
* these arithmetic checks cannot authenticate compaction or free UNKNOWN. */
|
|
export function pollStorageUsageRemaining(raw: PollStorageUsage, scope: "global"|"owner", rawPolicy:PollStoragePolicy=SERVER_POLL_STORAGE_POLICY):number|undefined {
|
|
try {
|
|
const policy=pollStoragePolicy(rawPolicy), value=usage(raw)
|
|
if(scope!=="global"&&scope!=="owner")refuse()
|
|
const attempts=scope==="global"?policy.globalAttempts:policy.ownerAttempts
|
|
const bytes=scope==="global"?policy.globalBytes:policy.ownerBytes, rows=scope==="global"?policy.globalRows:policy.ownerRows
|
|
if(value.attempts>attempts||value.bytes>bytes||value.rows>rows)refuse()
|
|
return Math.min(attempts-value.attempts,Math.floor((bytes-value.bytes)/(policy.attemptBytes+policy.admissionBytes)),Math.floor((rows-value.rows)/policy.attemptRows))
|
|
}catch{return undefined}
|
|
}
|
|
/** Exact counters, including full reservations and paid retained archives. */
|
|
export function pollStorageQuotaFit(globalUsage: PollStorageUsage, ownerUsage: PollStorageUsage,
|
|
rawPolicy: PollStoragePolicy = SERVER_POLL_STORAGE_POLICY): boolean {
|
|
try {
|
|
const policy = pollStoragePolicy(rawPolicy)
|
|
const global=usage(globalUsage),owner=usage(ownerUsage)
|
|
return (pollStorageUsageRemaining(global,"global",policy)??0)>0 && (pollStorageUsageRemaining(owner,"owner",policy)??0)>0
|
|
&& owner.attempts<=global.attempts && owner.bytes<=global.bytes && owner.rows<=global.rows
|
|
&& owner.compactedAttempts<=global.compactedAttempts && owner.archiveBytes<=global.archiveBytes
|
|
&& global.archiveBytes-owner.archiveBytes >= (global.compactedAttempts-owner.compactedAttempts)*40
|
|
&& global.archiveBytes-owner.archiveBytes <= (global.compactedAttempts-owner.compactedAttempts)*131072
|
|
} catch { return false }
|
|
}
|
|
|
|
export interface PollStorageGcCandidate {
|
|
scope: PollStorageScope
|
|
lastActivityAt: number
|
|
gcNotBefore: number
|
|
retentionDays: number
|
|
safetyWindowMs: number
|
|
/** Caller must verify the actual operation, never infer terminality from a receipt. */
|
|
mcpTerminal: boolean
|
|
}
|
|
/** No forced-delete branch: malformed/overflowed dates and live MCP operations
|
|
* remain reserved. Increasing retention applies to already persisted attempts. */
|
|
export function pollStorageGcEligible(candidate: PollStorageGcCandidate, rawPolicy: PollStoragePolicy, now: number): boolean {
|
|
try {
|
|
const policy = pollStoragePolicy(rawPolicy)
|
|
if (!record(candidate) || !timestamp(now)) return false
|
|
const scope = pollStorageScope(candidate.scope, { now, allowExpired: true })
|
|
if (!timestamp(candidate.lastActivityAt) || candidate.lastActivityAt < scope.issuedAt || candidate.lastActivityAt > now
|
|
|| !timestamp(candidate.gcNotBefore) || !days(candidate.retentionDays) || !integer(candidate.safetyWindowMs)
|
|
|| candidate.safetyWindowMs < 60000 || candidate.safetyWindowMs > DAY_MS || typeof candidate.mcpTerminal !== "boolean"
|
|
|| scope.kind === "mcp" && !candidate.mcpTerminal) return false
|
|
const retainedUntil = Math.max(scope.expiresAt, candidate.lastActivityAt)
|
|
+ Math.max(policy.retentionDays, candidate.retentionDays) * DAY_MS + Math.max(policy.safetyWindowMs, candidate.safetyWindowMs)
|
|
return timestamp(retainedUntil) && now > Math.max(candidate.gcNotBefore, retainedUntil)
|
|
} catch { return false }
|
|
}
|