/** * @pwngh/economy-lab * * Copyright (c) Preston Neal * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ import type { Outcome, Rejection, RejectionDetail, Success } from './contract.js'; /** * Reasons a well-formed request is declined on a healthy system, returned as a `rejected` * Outcome rather than thrown. Keeping ordinary "no" answers off the thrown-error path keeps * them out of error dashboards and alerts. * * @see {@link https://economy-lab-docs.pages.dev/economy/reference/outcomes-and-reason-codes/ * Outcomes & reason codes} for the full taxonomy. */ export type RejectionCode = 'INSUFFICIENT_FUNDS' | 'RISK_DENIED' | 'FUNDS_IMMATURE' | 'NOT_ENTITLED' | 'UNKNOWN_ORDER' | 'DUPLICATE_ORDER' | 'UNKNOWN_SUBSCRIPTION' | 'ALREADY_SUBSCRIBED' | 'BELOW_MINIMUM' | 'PAYOUT_TOO_SOON' | 'PAYEE_UNVERIFIED' | 'ECONOMY_PAUSED'; /** Every {@link RejectionCode}, enumerable for docs and exhaustiveness checks. */ export declare const REJECTION_CODES: readonly ["INSUFFICIENT_FUNDS", "RISK_DENIED", "FUNDS_IMMATURE", "NOT_ENTITLED", "UNKNOWN_ORDER", "DUPLICATE_ORDER", "UNKNOWN_SUBSCRIPTION", "ALREADY_SUBSCRIBED", "BELOW_MINIMUM", "PAYOUT_TOO_SOON", "PAYEE_UNVERIFIED", "ECONOMY_PAUSED"]; /** * Per-code registry of the fields each rejection's `detail` carries. The mapped key set and the * keyof-derived field lists are both compile-locked to {@link RejectionDetail}, so this catalog * cannot drift from the union. */ export declare const REJECTION_SPEC: { readonly [K in RejectionCode]: { readonly fields: readonly Exclude, 'reason'>[]; }; }; /** * Codes for thrown faults, as opposed to the expected "no" answers in {@link RejectionCode}. * Each value is a stable, namespaced string (e.g. `LEDGER.OVERDRAFT`); always reference these * constants, never the bare strings. * * @see {@link https://economy-lab-docs.pages.dev/economy/reference/outcomes-and-reason-codes/ * Outcomes & reason codes} for how each code maps to an HTTP status and retry decision. */ export declare const ERROR_CODES: { /** The request was structurally wrong (missing or invalid fields). */ readonly MALFORMED_OPERATION: "OP.MALFORMED"; /** A money amount was invalid (for example, negative or not a whole minor unit). */ readonly INVALID_AMOUNT: "MONEY.INVALID_AMOUNT"; /** * A money amount fell outside the signed 64-bit range the ledger's `BIGINT` columns store, * enforced at construction instead of at the database. */ readonly AMOUNT_OVERFLOW: "MONEY.OVERFLOW"; /** A posting's debits and credits didn't add up to zero, so the books wouldn't balance. */ readonly LEDGER_UNBALANCED: "LEDGER.UNBALANCED"; /** A posting named an account the ledger has no row for and won't create implicitly. */ readonly UNKNOWN_ACCOUNT: "LEDGER.UNKNOWN_ACCOUNT"; /** A single posting tried to combine two different currencies. */ readonly CURRENCY_MISMATCH: "LEDGER.CURRENCY_MISMATCH"; /** * A balance that's never supposed to go negative did. Ordinary shortfalls are declined up * front as INSUFFICIENT_FUNDS, so reaching this fault means a bug let a balance slip below * zero. */ readonly OVERDRAFT: "LEDGER.OVERDRAFT"; /** * A posting tried to mix custodial funds (money the platform owes users and must hold real money * against) with funds it does not owe, such as revenue. Those two kinds must stay in separate * accounts, so this is a thrown safety fault deep in the treasury path, never an expected "no". */ readonly COMMINGLING: "LEDGER.COMMINGLING"; readonly INVALID_TRANSITION: "SAGA.INVALID_TRANSITION"; /** * A netting session that already settled was asked to take another movement. Settlement txn * ids derive from the session id, so a settled session can never safely settle again; the * caller rotates to a new session id (epoch) instead. */ readonly SESSION_SETTLED: "SESSION.SETTLED"; /** * A scope's traffic reached a node its router assignment doesn't name. Accepting it would * fork the scope's single-writer lane, so the node refuses; the caller re-sends to the owner * named in `detail.owner`. */ readonly SESSION_MISROUTED: "SESSION.MISROUTED"; readonly UNAUTHORIZED: "AUTH.UNAUTHORIZED"; /** * A cryptographic signature didn't verify. Thrown in src/server.ts when an inbound webhook's * HMAC signature fails to match, before any state is changed; outer layers map this to HTTP 401. */ readonly INVALID_SIGNATURE: "AUTH.INVALID_SIGNATURE"; /** The storage layer failed. Reserved for the store paths that mean it (see normalizeError). */ readonly STORE_FAILURE: "STORE.FAILURE"; /** * An external provider or injected port failed: a payout rail, a dispatcher, a float or * reconcile feed. Dead-letter reasons carry this so the operator pages the right owner. */ readonly PROVIDER_FAILURE: "PROVIDER.FAILURE"; /** * Configuration failed to load or validate. Thrown at startup so a bad config stops * the service immediately rather than failing later. */ readonly CONFIG_INVALID: "CONFIG.INVALID"; /** * The hash chain failed to verify: a stored hash no longer matches the one recomputed from its * posting, so the ledger has been tampered with. Thrown before a checkpoint is signed, so no * attestation is produced over a broken chain. Last-resort integrity fault, never an expected * "no", hence a thrown fault rather than a RejectionCode. */ readonly CHAIN_BROKEN: "CHAIN.BROKEN"; }; /** * The union of every {@link ERROR_CODES} value, so a caller matching on `error.code` gets * autocompletion and exhaustiveness instead of comparing against free-typed strings. */ export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]; /** * The thrown-error type for every fault, carrying one stable {@link ERROR_CODES} code that * outer layers use to pick an HTTP status and a retry decision. `retryable` defaults to false: * only a throw site that knows a retry is safe sets it. * * Only `message` is safe to show a caller; `detail` and `cause` are for logging only. */ export declare class EconomyError extends Error { readonly code: ErrorCode; readonly retryable: boolean; readonly detail: Record; constructor(code: ErrorCode, message: string, options?: { cause?: unknown; retryable?: boolean; detail?: Record; }); } /** * Builds an {@link EconomyError} for the caller to throw — the constructor shorthand every throw * site in the package uses. It returns the error rather than throwing, so a site can attach it * to a dead-letter or reject a promise with it. */ export declare function fault(code: ErrorCode, message: string, options?: { cause?: unknown; retryable?: boolean; detail?: Record; }): EconomyError; /** * Builds a `rejected` Outcome: the value an operation returns (not throws) when it declines a * valid request for one of the expected business reasons in {@link RejectionCode}. The generic * pins `fields` to exactly the arm the reason selects. */ export declare function rejected(reason: K, fields: Omit, 'reason'>): Rejection; /** * True for `committed` and `duplicate` — both carry the committed transaction. A duplicate is * a success replayed: an idempotent redelivery handed the original receipt, so a caller that * only cares whether the money moved treats the two alike. */ export declare function isSuccess(outcome: Outcome): outcome is Success; /** * True for the `rejected` arm — an expected business "no", not a fault. The reason and its * typed fields are on `outcome.detail`. */ export declare function isRejection(outcome: Outcome): outcome is Rejection; /** * Narrows to a success or throws a plain Error naming the rejection — for hosts and tests where * a decline is unexpected. Deliberately not an {@link EconomyError}: an assertion failure has no * fault code or retry policy. * * @example * const outcome = await economy.submit(operation); * const { transaction } = requireSuccess(outcome); * // transaction.id names the posting whether the outcome was committed or a duplicate replay */ export declare function requireSuccess(outcome: Outcome): Success; /** * Turns anything caught in a `catch` into an {@link EconomyError}. If it's already one, returns * it unchanged: re-wrapping could overwrite its retryable flag and wrongly mark a non-retryable * failure as safe to retry. Anything else (a raw exception from a library, the storage layer, * etc.) is wrapped as a retryable STORE.FAILURE, with the original kept in `cause` for logs so * the caller never sees the raw error or its stack trace. * * @example * try { * await store.transact(accounts, work); * } catch (error) { * throw normalizeError(error); // an EconomyError either way; raw throws become STORE.FAILURE * } */ export declare function normalizeError(error: unknown): EconomyError; /** * {@link normalizeError} for a call into an injected port (a dispatcher, an applier, a feed): a * raw throw wraps as retryable PROVIDER.FAILURE instead of STORE.FAILURE, so a dead-letter * reason or failure log names the failing subsystem rather than blaming storage. */ export declare function normalizePortError(error: unknown): EconomyError; /** * The HTTP status an {@link EconomyError} maps to: 401 for auth/signature failures, 400 for a * caller-fixable bad request, 503 for a retryable fault, 500 otherwise. The canonical mapping * createServer applies, exposed so a host running its own endpoint answers the same way. */ export declare function statusForError(error: EconomyError): number;