@book.dev/sdk 3.5.0 → 3.6.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.
Files changed (44) hide show
  1. package/dist/backup.d.ts +75 -1
  2. package/dist/backup.js +8 -1
  3. package/dist/backup.js.map +1 -1
  4. package/dist/client.d.ts +134 -2
  5. package/dist/client.js +171 -0
  6. package/dist/client.js.map +1 -1
  7. package/dist/csv.d.ts +49 -0
  8. package/dist/csv.js +116 -0
  9. package/dist/csv.js.map +1 -0
  10. package/dist/importAssets.d.ts +10 -0
  11. package/dist/importAssets.js +17 -0
  12. package/dist/importAssets.js.map +1 -1
  13. package/dist/index.d.ts +10 -4
  14. package/dist/index.js +9 -3
  15. package/dist/index.js.map +1 -1
  16. package/dist/ledger.d.ts +772 -0
  17. package/dist/ledger.js +493 -0
  18. package/dist/ledger.js.map +1 -0
  19. package/dist/ledgerBeancount.d.ts +117 -0
  20. package/dist/ledgerBeancount.js +309 -0
  21. package/dist/ledgerBeancount.js.map +1 -0
  22. package/dist/ledgerBeancountFixture.d.ts +61 -0
  23. package/dist/ledgerBeancountFixture.js +459 -0
  24. package/dist/ledgerBeancountFixture.js.map +1 -0
  25. package/dist/ledgerCsv.d.ts +76 -0
  26. package/dist/ledgerCsv.js +197 -0
  27. package/dist/ledgerCsv.js.map +1 -0
  28. package/dist/money.d.ts +179 -0
  29. package/dist/money.js +283 -0
  30. package/dist/money.js.map +1 -0
  31. package/dist/notionImport.d.ts +5 -3
  32. package/dist/notionImport.js +6 -56
  33. package/dist/notionImport.js.map +1 -1
  34. package/dist/plugins.d.ts +24 -0
  35. package/dist/plugins.js +25 -0
  36. package/dist/plugins.js.map +1 -1
  37. package/dist/provenance.d.ts +17 -0
  38. package/dist/provenance.js.map +1 -1
  39. package/dist/routes.d.ts +121 -0
  40. package/dist/routes.js +122 -0
  41. package/dist/routes.js.map +1 -1
  42. package/dist/types.d.ts +8 -0
  43. package/dist/types.js.map +1 -1
  44. package/package.json +1 -1
@@ -0,0 +1,772 @@
1
+ /**
2
+ * Ledger contract (LGR-3): the shared types, typed errors, and pure audit-replay
3
+ * reducer for the server-enforced double-entry ledger.
4
+ *
5
+ * Data model: ledger data lives in FOUR server-managed OpenBook databases
6
+ * (accounts / transactions / postings / reconciliations) seeded on a restricted
7
+ * host page. Rows are ordinary pages; every ledger value is a database property.
8
+ * The server's `LedgerStore` is the ONLY writer — the generic page/row mutation
9
+ * surface (HTTP routes AND direct store calls) rejects writes to ledger rows, so
10
+ * the invariants hold in both server mode and browser-local (PGlite) mode.
11
+ *
12
+ * Money: amounts are SIGNED INTEGER MINOR UNITS (LGR-2, see `./money`). Amounts
13
+ * on the wire are integers — never parsed strings.
14
+ */
15
+ /** Machine-readable ledger error categories. */
16
+ export type LedgerErrorCode = 'not-initialized' | 'not-found' | 'managed' | 'immutable' | 'unbalanced' | 'too-few-postings' | 'invalid-amount' | 'account-not-found' | 'account-closed' | 'currency-mismatch' | 'invalid-state' | 'nonzero-balance' | 'reconciled-locked' | 'reconciliation-exists' | 'reconciliation-unbalanced' | 'posting-not-reconcilable' | 'reconciliation-too-large' | 'period-closed' | 'period-overlap' | 'period-out-of-order' | 'period-close-conflict' | 'evidence-required' | 'invalid-input';
17
+ /**
18
+ * HTTP status the API maps each {@link LedgerErrorCode} to.
19
+ *
20
+ * The three LGR-11 reconciliation rejections are 409, not 400: they are all
21
+ * "not valid for the CURRENT state of the book" rather than malformed requests.
22
+ * The identical call becomes legal once the open reconciliation is finished,
23
+ * once the difference reaches zero, or once the posting is posted — which is
24
+ * what tells a client to re-read and retry instead of fixing its payload.
25
+ */
26
+ export declare function ledgerErrorStatus(code: LedgerErrorCode): 400 | 403 | 404 | 409;
27
+ /**
28
+ * Typed ledger error. The server REJECTS invariant violations with these (never
29
+ * advises); the HTTP layer maps them to `{error, code}` bodies via
30
+ * {@link ledgerErrorStatus} and `HttpDataClient` re-materializes them, so a
31
+ * caller catches the same error class over either transport.
32
+ */
33
+ export declare class LedgerError extends Error {
34
+ readonly code: LedgerErrorCode;
35
+ constructor(code: LedgerErrorCode, message: string);
36
+ }
37
+ /** The set of valid codes, for wire-body → {@link LedgerError} re-materialization. */
38
+ export declare const LEDGER_ERROR_CODES: readonly LedgerErrorCode[];
39
+ export declare const LEDGER_ACCOUNT_TYPES: readonly ["asset", "liability", "equity", "revenue", "expense"];
40
+ export type LedgerAccountType = (typeof LEDGER_ACCOUNT_TYPES)[number];
41
+ export declare const LEDGER_ACCOUNT_STATUSES: readonly ["open", "closed"];
42
+ export type LedgerAccountStatus = (typeof LEDGER_ACCOUNT_STATUSES)[number];
43
+ export declare const LEDGER_TRANSACTION_STATES: readonly ["draft", "posted", "void"];
44
+ export type LedgerTransactionState = (typeof LEDGER_TRANSACTION_STATES)[number];
45
+ export declare const LEDGER_CLEARED_STATES: readonly ["pending", "cleared", "reconciled"];
46
+ export type LedgerClearedState = (typeof LEDGER_CLEARED_STATES)[number];
47
+ /**
48
+ * A reconciliation's lifecycle (LGR-11 + LGR-22).
49
+ *
50
+ * `abandoned` is a TERMINAL status, not a deletion: a statement someone opened
51
+ * against the wrong closing balance still happened, and the record of it — with
52
+ * the audit events that amended and ended it — is what lets a later reader tell
53
+ * "this account was never reconciled" apart from "this attempt was given up on".
54
+ * It is invisible to the one-open-per-account rule (which asks only for `open`),
55
+ * so abandoning immediately frees the account for a fresh start.
56
+ */
57
+ export declare const LEDGER_RECONCILIATION_STATUSES: readonly ["open", "finished", "abandoned"];
58
+ export type LedgerReconciliationStatus = (typeof LEDGER_RECONCILIATION_STATUSES)[number];
59
+ /**
60
+ * A period's lifecycle (LGR-12). `reopened` is TERMINAL for the RECORD, not for
61
+ * the range: reopening keeps the row (with the reversal that voided its closing
62
+ * entry) so the settings UI and the audit trail can show that a close happened
63
+ * and was undone; closing the range again writes a NEW period record. Only
64
+ * `closed` periods lock dates.
65
+ */
66
+ export declare const LEDGER_PERIOD_STATUSES: readonly ["closed", "reopened"];
67
+ export type LedgerPeriodStatus = (typeof LEDGER_PERIOD_STATUSES)[number];
68
+ /**
69
+ * The account types a period close sweeps into retained earnings (LGR-12): the
70
+ * income-statement ("flow") accounts. ONE list, exported, because the server's
71
+ * closing-entry generator and any report fold that classifies flow accounts
72
+ * must agree — this epic's recurring defect is two copies of one fact.
73
+ */
74
+ export declare const LEDGER_INCOME_STATEMENT_ACCOUNT_TYPES: readonly ["revenue", "expense"];
75
+ /** Whether `type` names an income-statement (flow) account. */
76
+ export declare function isIncomeStatementAccountType(type: unknown): type is 'revenue' | 'expense';
77
+ /**
78
+ * The server's HARD CAP on one `listTransactions` page: a larger `limit` is
79
+ * clamped to this, silently.
80
+ *
81
+ * Exported because a reader that totals what it fetched — a report — cannot
82
+ * tell a full page from a complete book without it, and a duplicated literal
83
+ * would rot: if this cap ever dropped, a report asking for the old number would
84
+ * be clamped, conclude it had read everything, and render a PARTIAL total as a
85
+ * complete one. Both the server clamp and the ledger plugin's report read this
86
+ * one constant, and `ledger.test.ts` asserts the clamp actually holds.
87
+ */
88
+ export declare const LEDGER_MAX_TRANSACTION_LIMIT = 1000;
89
+ /** The page size `listTransactions` uses when the caller names none. */
90
+ export declare const LEDGER_DEFAULT_TRANSACTION_LIMIT = 500;
91
+ /** Stable property ids for the seeded ledger database schemas. */
92
+ export declare const LEDGER_PROP: {
93
+ readonly account: {
94
+ readonly type: "lp_type";
95
+ readonly status: "lp_status";
96
+ readonly currency: "lp_currency";
97
+ /** LGR-14: `true` on an account that refuses to accept a posting from an
98
+ * entry with no evidence attached; absent everywhere else (additive — no
99
+ * migration, pre-LGR-14 accounts read back `false`). */
100
+ readonly evidenceRequired: "lp_evidence_required";
101
+ };
102
+ readonly transaction: {
103
+ readonly date: "lp_date";
104
+ readonly description: "lp_description";
105
+ readonly state: "lp_state";
106
+ readonly postedAt: "lp_posted_at";
107
+ readonly postedBy: "lp_posted_by";
108
+ readonly reverses: "lp_reverses";
109
+ readonly evidence: "lp_evidence";
110
+ readonly entryNo: "lp_entry_no";
111
+ /** LGR-12: `'closing'` on a period-close entry; absent on ordinary entries. */
112
+ readonly kind: "lp_kind";
113
+ };
114
+ readonly posting: {
115
+ readonly transaction: "lp_transaction";
116
+ readonly account: "lp_account";
117
+ readonly amount: "lp_amount_minor";
118
+ readonly cleared: "lp_cleared";
119
+ readonly reconciliation: "lp_reconciliation";
120
+ readonly memo: "lp_memo";
121
+ };
122
+ readonly reconciliation: {
123
+ readonly account: "lp_account";
124
+ readonly statementDate: "lp_statement_date";
125
+ readonly statementBalance: "lp_statement_balance_minor";
126
+ readonly status: "lp_status";
127
+ };
128
+ };
129
+ /**
130
+ * One piece of evidence attached to a transaction (LGR-14): the post-time
131
+ * manifest entry. `sha256` IS the asset-store id (assets are content-addressed
132
+ * — the id is the SHA-256 hex of the bytes), so "which file" and "which bytes"
133
+ * are one fact: replacing the stored bytes without changing this hash is
134
+ * impossible through any API, and detecting a direct-SQL replacement is one
135
+ * re-hash (the verifier's evidence check). `filename` is the display name the
136
+ * uploader gave it; `size` is the asset store's byte count, resolved
137
+ * server-side at attach time — never client-supplied.
138
+ *
139
+ * On a POSTED transaction the manifest is frozen with the rest of the entry
140
+ * (it is inside `transactionContent`, so inside the audit before/after hashes).
141
+ */
142
+ export interface LedgerEvidence {
143
+ filename: string;
144
+ sha256: string;
145
+ size: number;
146
+ }
147
+ /**
148
+ * One evidence attachment as a CLIENT names it (LGR-14): the content-hash id
149
+ * of an already-uploaded asset plus a display filename. No `size` — the server
150
+ * resolves it from the asset store (measuring the bytes, not trusting a cached
151
+ * column), so a manifest can never claim a byte count the store does not hold.
152
+ *
153
+ * CONFIDENTIALITY (accepted, stated): an asset inherits the read gate of EVERY
154
+ * page that references it. Attaching refs the asset to the ledger's own
155
+ * transaction row, but the UPLOAD already ref'd it to the page it was uploaded
156
+ * from — so a receipt uploaded from a widely-shared page stays readable to
157
+ * that page's audience for as long as that page references it. That is the
158
+ * platform's standing asset semantics; this feature puts receipts into them.
159
+ * Upload sensitive receipts from pages whose audience is the ledger's.
160
+ */
161
+ export interface LedgerEvidenceInput {
162
+ /** The asset-store id: 64 lowercase hex chars — the SHA-256 of the bytes. */
163
+ sha256: string;
164
+ filename: string;
165
+ }
166
+ /** A ledger account. `name` is hierarchical, colon-delimited (`Assets:Bank:Checking`). */
167
+ export interface LedgerAccount {
168
+ id: string;
169
+ name: string;
170
+ type: LedgerAccountType;
171
+ status: LedgerAccountStatus;
172
+ /** ISO-4217-shaped currency code. Defaults to `USD`. */
173
+ currency: string;
174
+ /**
175
+ * LGR-14: when `true`, posting an entry with a leg on this account is
176
+ * REJECTED (`evidence-required`) unless the entry has evidence attached.
177
+ * `false` is what every account written before LGR-14 reads back as (the
178
+ * stored key is simply absent — additive, no migration). Reversals and
179
+ * server-generated closing entries are exempt: a reversal CARRIES its
180
+ * original's manifest (F1), and a closing entry is derived arithmetic.
181
+ *
182
+ * PRESENCE-ONLY, by design: the gate asserts that some file was attached at
183
+ * post time, not that it is the right one — the badge and the manifest read
184
+ * as "this entry can answer with what was filed", never as attestation that
185
+ * the filing is correct or even relevant. The verifier's
186
+ * `evidence-required-missing` advisory reports entries that do not satisfy
187
+ * the CURRENT policy — turning this flag on flags history, deliberately.
188
+ */
189
+ evidenceRequired: boolean;
190
+ createdAt: string;
191
+ updatedAt: string;
192
+ }
193
+ /** One leg of a transaction. `amountMinor` is a signed integer of minor units. */
194
+ export interface LedgerPosting {
195
+ id: string;
196
+ transactionId: string;
197
+ accountId: string;
198
+ amountMinor: number;
199
+ cleared: LedgerClearedState;
200
+ reconciliationId: string | null;
201
+ /**
202
+ * Free-text note on THIS LEG (LGR-16) — "gross wages", the bank's raw
203
+ * statement line, the invoice number. Distinct from the transaction's
204
+ * `description`, which describes the entry as a whole; a compound entry's
205
+ * legs each carry their own. Length-capped exactly like `description`.
206
+ *
207
+ * `null` means no memo, and is what every posting written before LGR-16
208
+ * reads back as — the field is additive, so no migration is required.
209
+ */
210
+ memo: string | null;
211
+ }
212
+ /** A journal entry with its postings. Compound (n-ary) entries are first-class. */
213
+ export interface LedgerTransaction {
214
+ id: string;
215
+ /** ISO date (`YYYY-MM-DD`). */
216
+ date: string;
217
+ description: string;
218
+ state: LedgerTransactionState;
219
+ /** Set exactly once at posting; never mutated afterwards. */
220
+ postedAt: string | null;
221
+ postedBy: string | null;
222
+ /** The transaction this one reverses, when it is a reversal. */
223
+ reverses: string | null;
224
+ /** Server-assigned monotonic entry number (per library), set at posting. */
225
+ entryNo: number | null;
226
+ /**
227
+ * Entry kind (LGR-12). `'closing'` marks the server-generated period-close
228
+ * entry — the one that sweeps income-statement balances into retained
229
+ * earnings. `null` on every ordinary entry AND on every entry written before
230
+ * LGR-12 (the field is additive; no migration). Reports use it (plus the
231
+ * `reverses` link, for the reversal a reopen posts) to keep closing entries
232
+ * out of income-statement arithmetic.
233
+ */
234
+ kind: 'closing' | null;
235
+ /**
236
+ * The evidence manifest (LGR-14). On a DRAFT: what is attached so far, live.
237
+ * On a POSTED entry: the manifest snapshotted at post time — frozen with the
238
+ * entry (it is part of the audited content), never mutated afterwards.
239
+ * A REVERSAL carries its original's manifest verbatim (F1) — "a reversal's
240
+ * evidence is the original entry it undoes", made literal, so a reversal
241
+ * chain stays answerable end to end and double-reversing cannot launder the
242
+ * receipts off a live entry. Empty on entries written before LGR-14, on
243
+ * reversals of bare entries, and on server-generated closing entries.
244
+ */
245
+ evidence: LedgerEvidence[];
246
+ postings: LedgerPosting[];
247
+ createdAt: string;
248
+ updatedAt: string;
249
+ }
250
+ /**
251
+ * One statement reconciliation (LGR-11): an account, the statement it is being
252
+ * matched against, and whether that match has been FINISHED.
253
+ *
254
+ * `finished` is only reachable at a difference of exactly zero (statement
255
+ * balance − cleared balance = 0), and finishing FREEZES the postings it matched
256
+ * (`cleared: 'reconciled'` + `reconciliationId` set — invariant 4). Reopening is
257
+ * an explicit, audited step that unfreezes them again.
258
+ *
259
+ * While OPEN it is also correctable and abandonable (LGR-22): the target itself
260
+ * can be mistyped, and a wrong target is unreachable by definition — the
261
+ * difference can never be driven to zero, `finish` is therefore unreachable, and
262
+ * before LGR-22 the account could never be reconciled again. See
263
+ * {@link LedgerReconciliationPatch}.
264
+ */
265
+ export interface LedgerReconciliation {
266
+ id: string;
267
+ accountId: string;
268
+ /** ISO date (`YYYY-MM-DD`) the statement closes on. */
269
+ statementDate: string;
270
+ /**
271
+ * The statement's closing balance in SIGNED INTEGER MINOR UNITS, on the
272
+ * ledger's debit-positive convention (LGR-2) — the same convention every
273
+ * posting amount uses, so the difference is one subtraction and never a
274
+ * re-signing. A UI that asks for it on the account's NORMAL side is
275
+ * responsible for converting before it gets here.
276
+ */
277
+ statementBalanceMinor: number;
278
+ status: LedgerReconciliationStatus;
279
+ createdAt: string;
280
+ /** Bumped by every start/toggle-free mutation — finish and reopen included. */
281
+ updatedAt: string;
282
+ }
283
+ export interface LedgerReconciliationInput {
284
+ accountId: string;
285
+ /** ISO date (`YYYY-MM-DD`). */
286
+ statementDate: string;
287
+ /** Signed integer minor units, debit-positive. Never a parsed string. */
288
+ statementBalanceMinor: number;
289
+ }
290
+ /**
291
+ * AMEND an OPEN reconciliation (LGR-22): correct the statement it is being
292
+ * matched against, without touching a single posting.
293
+ *
294
+ * The account is deliberately NOT amendable. Changing it would leave the ticks
295
+ * already made pointing at another account's postings, which is not a
296
+ * correction but a different reconciliation — abandon this one and start that
297
+ * one. Everything here is optional, but a patch that names nothing is rejected
298
+ * (`invalid-input`): a mutation that changes nothing must not write an audit
299
+ * event claiming it did.
300
+ */
301
+ export interface LedgerReconciliationPatch {
302
+ /** ISO date (`YYYY-MM-DD`). */
303
+ statementDate?: string;
304
+ /** Signed integer minor units, debit-positive — the same convention as
305
+ * {@link LedgerReconciliationInput.statementBalanceMinor}. */
306
+ statementBalanceMinor?: number;
307
+ }
308
+ /**
309
+ * What a reconciliation looks like once its postings are counted — the exact
310
+ * arithmetic a bookkeeper checks, computed server-side so the Finish gate and
311
+ * the on-screen readout can never disagree.
312
+ */
313
+ export interface LedgerReconciliationSummary {
314
+ reconciliation: LedgerReconciliation;
315
+ /** Σ of every `cleared`/`reconciled` posting on the account, debit-positive. */
316
+ clearedBalanceMinor: number;
317
+ /** `statementBalanceMinor − clearedBalanceMinor`. Finish requires exactly 0. */
318
+ differenceMinor: number;
319
+ /** The postings this reconciliation matched (only meaningful once finished). */
320
+ matchedPostingIds: string[];
321
+ }
322
+ /**
323
+ * One accounting period close (LGR-12): an inclusive date range the store has
324
+ * LOCKED — no posting and no reversal may be DATED inside a `closed` period —
325
+ * plus the closing entry that swept the income-statement balances into
326
+ * retained earnings when the range was closed.
327
+ *
328
+ * Storage: the periods live in ONE `settings` row (`ledgerPeriods`), not in a
329
+ * fifth managed database. See `LedgerStore.closePeriod` for why (short form: a
330
+ * book seeded before LGR-12 can never grow a fifth database — the seed's adopt
331
+ * check returns early — and the row IS the lock the close flow serializes on,
332
+ * at the `settings` slot the proven lock order already holds).
333
+ */
334
+ export interface LedgerPeriod {
335
+ id: string;
336
+ /** Inclusive ISO date (`YYYY-MM-DD`) the period starts on. */
337
+ start: string;
338
+ /** Inclusive ISO date (`YYYY-MM-DD`) the period ends on. */
339
+ end: string;
340
+ status: LedgerPeriodStatus;
341
+ /**
342
+ * The closing entry posted when this period was closed, or `null` when the
343
+ * range held no income-statement balance to close (the range still locks).
344
+ * Kept after a reopen (the entry is `void` then — history, not a live claim).
345
+ */
346
+ closingEntryId: string | null;
347
+ /** The reversal that voided the closing entry at reopen; `null` until then. */
348
+ reopenEntryId: string | null;
349
+ closedAt: string;
350
+ closedBy: string;
351
+ reopenedAt: string | null;
352
+ reopenedBy: string | null;
353
+ }
354
+ /** `POST /api/ledger/periods` — close a period. */
355
+ export interface LedgerPeriodCloseInput {
356
+ /** Inclusive ISO date (`YYYY-MM-DD`). */
357
+ start: string;
358
+ /** Inclusive ISO date (`YYYY-MM-DD`); must not precede `start`. */
359
+ end: string;
360
+ /**
361
+ * The equity account the closing entry credits. Defaults to the account
362
+ * named `Equity:RetainedEarnings`; rejected `account-not-found` when neither
363
+ * is resolvable (create the account first — the starter chart carries it).
364
+ */
365
+ retainedEarningsAccountId?: string;
366
+ }
367
+ /** What a period close returns: the lock, the entry, and the WARNING list. */
368
+ export interface LedgerPeriodCloseResult {
369
+ period: LedgerPeriod;
370
+ /** `null` when the range had no income-statement balance to close. */
371
+ closingEntry: LedgerTransaction | null;
372
+ /**
373
+ * Reconciliations still OPEN with a statement dated on or before the close
374
+ * (warn-not-block by design): the close proceeds, and this names what is
375
+ * unfinished so the UI can say so instead of gating.
376
+ */
377
+ openReconciliations: LedgerReconciliation[];
378
+ }
379
+ /** What a period reopen returns: the unlocked period and the voiding reversal. */
380
+ export interface LedgerPeriodReopenResult {
381
+ period: LedgerPeriod;
382
+ /** `null` when the period was closed without a closing entry. */
383
+ reversal: LedgerTransaction | null;
384
+ }
385
+ /**
386
+ * THE period predicate (LGR-12), shared by the store's post/reverse guards and
387
+ * every UI surface that wants to say WHY a date is refused: the `closed` period
388
+ * containing `date`, or `null`. Pure; string comparison is safe because both
389
+ * sides are ISO `YYYY-MM-DD`.
390
+ */
391
+ export declare function closedPeriodContaining(periods: readonly LedgerPeriod[], date: string): LedgerPeriod | null;
392
+ /**
393
+ * Every CLOSED period overlapping the inclusive range `[from, to]` (`''` means
394
+ * open at that end) — the display-only "this report crosses a closed period"
395
+ * marker the report blocks render. Sorted by start date so the marker reads in
396
+ * calendar order.
397
+ */
398
+ export declare function closedPeriodsOverlapping(periods: readonly LedgerPeriod[], from: string, to: string): LedgerPeriod[];
399
+ /** The seeded ledger database ids + host page. */
400
+ export interface LedgerDatabases {
401
+ accounts: string;
402
+ transactions: string;
403
+ postings: string;
404
+ reconciliations: string;
405
+ }
406
+ /** `GET /api/ledger` — whether the ledger is initialized, and where it lives. */
407
+ export interface LedgerInfo {
408
+ exists: boolean;
409
+ hostPageId: string | null;
410
+ databases: LedgerDatabases | null;
411
+ }
412
+ export interface LedgerAccountInput {
413
+ name: string;
414
+ type: LedgerAccountType;
415
+ /** ISO-4217-shaped code; defaults to `USD`. */
416
+ currency?: string;
417
+ /** LGR-14: refuse postings from entries with no evidence. Default `false`. */
418
+ evidenceRequired?: boolean;
419
+ }
420
+ export interface LedgerAccountPatch {
421
+ name?: string;
422
+ /** `closed` is rejected while the account's posted balance is nonzero. */
423
+ status?: LedgerAccountStatus;
424
+ /**
425
+ * LGR-14: turn the evidence requirement on or off. Forward-looking only —
426
+ * flipping it on does not (and cannot) retro-flag entries already posted;
427
+ * the gate runs at post time.
428
+ */
429
+ evidenceRequired?: boolean;
430
+ }
431
+ export interface LedgerPostingInput {
432
+ accountId: string;
433
+ /** Signed integer minor units. Never a parsed string. */
434
+ amountMinor: number;
435
+ /** Initial cleared state; `reconciled` is not settable here. Default `pending`. */
436
+ cleared?: 'pending' | 'cleared';
437
+ /**
438
+ * Free-text note on this leg (LGR-16). Omitted, `undefined`, `null` and `''`
439
+ * all store as `null`; longer than the `description` cap is rejected with
440
+ * `invalid-input`.
441
+ */
442
+ memo?: string | null;
443
+ }
444
+ export interface LedgerDraftInput {
445
+ /** ISO date (`YYYY-MM-DD`). */
446
+ date: string;
447
+ description?: string;
448
+ postings?: LedgerPostingInput[];
449
+ /**
450
+ * Evidence to attach at creation (LGR-14). Each named asset must already be
451
+ * in the content-addressed asset store (upload first, attach by hash);
452
+ * `size` is resolved server-side. Duplicate hashes are rejected — the
453
+ * manifest is a set.
454
+ */
455
+ evidence?: LedgerEvidenceInput[];
456
+ }
457
+ export interface LedgerDraftPatch {
458
+ date?: string;
459
+ description?: string;
460
+ /** When present, REPLACES the draft's postings wholesale. */
461
+ postings?: LedgerPostingInput[];
462
+ /** When present, REPLACES the draft's evidence wholesale (LGR-14) — the
463
+ * same replacement contract `postings` uses. `[]` detaches everything. */
464
+ evidence?: LedgerEvidenceInput[];
465
+ }
466
+ export interface LedgerReverseOptions {
467
+ /** ISO date for the reversing entry; defaults to the original's date. */
468
+ date?: string;
469
+ description?: string;
470
+ }
471
+ export type LedgerAuditAction = 'ledger.init' | 'ledger.acl' | 'account.create' | 'account.update' | 'transaction.create' | 'transaction.update' | 'transaction.delete' | 'transaction.post' | 'transaction.reverse' | 'posting.cleared'
472
+ /** A statement reconciliation was opened on an account (LGR-11). */
473
+ | 'reconciliation.start'
474
+ /**
475
+ * A reconciliation reached a zero difference and was FINISHED (LGR-11): its
476
+ * matched postings froze at `reconciled`. ONE event covers the reconciliation
477
+ * row and every posting it froze — the `transaction.reverse` precedent, and
478
+ * for the same reason: they commit together or not at all.
479
+ */
480
+ | 'reconciliation.finish'
481
+ /** A finished reconciliation was explicitly REOPENED, unfreezing its postings. */
482
+ | 'reconciliation.reopen'
483
+ /**
484
+ * An OPEN reconciliation's statement date/balance was CORRECTED (LGR-22).
485
+ * Touches the reconciliation row and nothing else — no posting changes ride
486
+ * along, which is exactly why this is not folded into `.start`: the trail has
487
+ * to show that the target moved, and to what.
488
+ */
489
+ | 'reconciliation.amend'
490
+ /**
491
+ * An OPEN reconciliation was ABANDONED (LGR-22) — given up on rather than
492
+ * balanced. Terminal, and posting-neutral: every tick keeps the cleared state
493
+ * it had, because a tick records that the posting appeared on the bank, which
494
+ * abandoning the match does not un-observe.
495
+ */
496
+ | 'reconciliation.abandon'
497
+ /**
498
+ * A period was CLOSED (LGR-12): the date range locked, and — when the range
499
+ * held income-statement balances — the closing entry posted. ONE event covers
500
+ * the period record and the entry it posted (the `transaction.reverse` /
501
+ * `reconciliation.finish` precedent: they commit together or not at all).
502
+ * The payload's `openReconciliationIds` names what the warn-not-block check
503
+ * surfaced; advisory context, not entity content.
504
+ */
505
+ | 'period.close'
506
+ /**
507
+ * A closed period was explicitly REOPENED (LGR-12): the range unlocked and
508
+ * the closing entry voided via a reversal posted in the SAME event —
509
+ * `transaction`/`originalId` carry the reversal pair exactly as
510
+ * `transaction.reverse` does.
511
+ */
512
+ | 'period.reopen'
513
+ /** The ledger auto-export target was set or cleared (LGR-7). Policy, not
514
+ * ledger content — it touches no entity, so a replay ignores it, but it is
515
+ * recorded here so the book itself carries evidence of where copies go. */
516
+ | 'ledger.autoExportPath'
517
+ /**
518
+ * A backup bundle's ledger history was RESTORED into this library (LGR-15).
519
+ * Appended ON TOP of the restored tail — chained from it — naming the actor
520
+ * and the bundle's content hash, so an installed history is always bracketed
521
+ * by an attributable event rather than ending exactly where the bundle ends.
522
+ * Touches no entity (a replay ignores it), but its recorded afterHash is
523
+ * re-derived from its own payload by the verifier, like `autoExportPath`.
524
+ *
525
+ * VERSION NOTE (deliberate): builds that predate this action REFUSE to read
526
+ * streams containing it — the unknown-action rejection in `auditFromRow` is
527
+ * the fail-closed posture, so a restored library is not silently
528
+ * mis-replayed by an old build; restore such a bundle on a current build.
529
+ */
530
+ | 'ledger.restore';
531
+ /**
532
+ * Every known audit action — the validation set for a stored `action` value.
533
+ *
534
+ * `as const satisfies` (not a widened annotation) so the array's element type is
535
+ * the literal union: adding a member to {@link LedgerAuditAction} without adding
536
+ * it here is then caught by the exhaustiveness check below at COMPILE time,
537
+ * rather than surfacing at runtime as a log this build refuses to interpret.
538
+ */
539
+ export declare const LEDGER_AUDIT_ACTIONS: readonly ["ledger.init", "ledger.acl", "account.create", "account.update", "transaction.create", "transaction.update", "transaction.delete", "transaction.post", "transaction.reverse", "posting.cleared", "reconciliation.start", "reconciliation.finish", "reconciliation.reopen", "reconciliation.amend", "reconciliation.abandon", "period.close", "period.reopen", "ledger.autoExportPath", "ledger.restore"];
540
+ /**
541
+ * One append-only audit event. `payload` carries the full after-content of the
542
+ * touched entity (so the stream is REPLAYABLE — see {@link replayLedgerAudit});
543
+ * `beforeHash`/`afterHash` are SHA-256 hex of the canonical entity content
544
+ * before/after the mutation (`null` where there is no before/after state).
545
+ */
546
+ export interface LedgerAuditEvent {
547
+ /** Server-assigned, strictly increasing sequence (the pagination cursor). */
548
+ seq: number;
549
+ id: string;
550
+ actorSubject: string;
551
+ actorName: string;
552
+ action: LedgerAuditAction;
553
+ /** The entity ids this event touches (transaction + original for a reverse). */
554
+ entityIds: string[];
555
+ payload: Record<string, unknown>;
556
+ beforeHash: string | null;
557
+ afterHash: string | null;
558
+ /**
559
+ * TAMPER-EVIDENCE (LGR-3): the {@link ledgerAuditEventHash} of the immediately
560
+ * preceding event, written in the SAME transaction as this one — so the log is
561
+ * a hash chain, not merely an append-only table. `null` only for the genesis
562
+ * event. Append-only-by-construction stops the API; the chain additionally
563
+ * makes an UNRECOMPUTED database-level edit or middle deletion detectable, and
564
+ * lets a BIGSERIAL gap be told apart from a rolled-back transaction. It does
565
+ * NOT detect head/tail truncation or a fully recomputed rewrite — see
566
+ * {@link verifyLedgerAuditChain} for the precise guarantee before relying on
567
+ * a green result.
568
+ */
569
+ prevHash: string | null;
570
+ createdAt: string;
571
+ }
572
+ /** The state a replay of the audit stream reconstructs. */
573
+ export interface LedgerReplayState {
574
+ initialized: boolean;
575
+ accounts: Record<string, LedgerAccount>;
576
+ transactions: Record<string, LedgerTransaction>;
577
+ /** Statement reconciliations (LGR-11), keyed by id. */
578
+ reconciliations: Record<string, LedgerReconciliation>;
579
+ /** Closed/reopened periods (LGR-12), keyed by id. */
580
+ periods: Record<string, LedgerPeriod>;
581
+ }
582
+ /**
583
+ * One posting's cleared state as a `reconciliation.finish` / `.reopen` event
584
+ * records it. The event carries the FULL after-state of every posting it
585
+ * touched, because that is what makes the stream replayable: a reader that only
586
+ * knew "these ids were frozen" could not reconstruct the `reconciliationId` a
587
+ * reopen must clear again.
588
+ */
589
+ export interface LedgerReconciliationPostingChange {
590
+ postingId: string;
591
+ transactionId: string;
592
+ cleared: LedgerClearedState;
593
+ reconciliationId: string | null;
594
+ }
595
+ /**
596
+ * Pure reducer: fold the audit event stream (ascending `seq`) into the expected
597
+ * current ledger state. Deterministic and side-effect free — the replay test
598
+ * compares this against the live store to prove the audit log is a complete
599
+ * record of every ledger mutation.
600
+ */
601
+ export declare function replayLedgerAudit(events: Iterable<LedgerAuditEvent>): LedgerReplayState;
602
+ /**
603
+ * The tamper-evidence hash of ONE audit event: SHA-256 over its canonical
604
+ * content INCLUDING `prevHash`, which is what links each event to its
605
+ * predecessor. Async because it uses WebCrypto (isomorphic: Node, browser,
606
+ * sidecar). `seq` is deliberately part of the hashed content, so renumbering
607
+ * events breaks the chain just as rewriting one does.
608
+ */
609
+ export declare function ledgerAuditEventHash(event: LedgerAuditEvent): Promise<string>;
610
+ /** The verdict of {@link verifyLedgerAuditChain}. */
611
+ export interface LedgerAuditChainResult {
612
+ ok: boolean;
613
+ /** How many events were checked. */
614
+ checked: number;
615
+ /** The `seq` of the first event whose link is broken, when `ok` is false. */
616
+ brokenAtSeq: number | null;
617
+ reason: string | null;
618
+ }
619
+ /**
620
+ * Verify the audit log's hash chain over `events` (ASCENDING `seq`, contiguous
621
+ * from the genesis event). Each event must carry its predecessor's
622
+ * {@link ledgerAuditEventHash} in `prevHash`; the first must carry `null`.
623
+ *
624
+ * THE PRECISE GUARANTEE — a green result is NOT "this log is authentic":
625
+ *
626
+ * - DETECTED: any edit, reordering, or middle deletion whose perpetrator does
627
+ * not also recompute every following link. That covers the realistic
628
+ * accident-and-opportunist cases (a stray `UPDATE`, a row deleted to hide one
629
+ * entry), and it distinguishes a deleted event from the innocuous BIGSERIAL
630
+ * gap a rolled-back transaction leaves.
631
+ * - NOT DETECTED: truncation of the HEAD or the TAIL — a prefix or suffix of
632
+ * the log removed wholesale still verifies clean, because nothing in the data
633
+ * states where the chain is supposed to start or end. Nor a WHOLESALE REWRITE:
634
+ * this hash is unkeyed and this function is exported, so an actor with
635
+ * database access can rewrite every event and recompute every link.
636
+ *
637
+ * Detecting those requires an off-box ANCHOR — periodically publishing the tail
638
+ * hash somewhere the database's owner cannot rewrite — which is tracked
639
+ * separately (LGR-18) and is not implemented here. Treat this as integrity
640
+ * against tampering that does not control the whole log, not as a signature.
641
+ *
642
+ * Verifying a PAGE of the log (not starting at genesis) is supported: pass the
643
+ * slice and it checks the links WITHIN it, reporting the first break.
644
+ */
645
+ export declare function verifyLedgerAuditChain(events: readonly LedgerAuditEvent[]): Promise<LedgerAuditChainResult>;
646
+ /**
647
+ * Canonical JSON of an entity for audit content-hashing: keys sorted at every
648
+ * depth, so a byte-identical serialization is independent of insertion order.
649
+ */
650
+ export declare function canonicalLedgerJson(value: unknown): string;
651
+ /**
652
+ * Validate a hierarchical, colon-delimited account name (`Assets:Bank:Checking`):
653
+ * one or more non-empty, non-whitespace-only segments separated by single colons.
654
+ */
655
+ export declare function isValidLedgerAccountName(name: unknown): name is string;
656
+ /** Validate an ISO `YYYY-MM-DD` date string (a real calendar date). */
657
+ export declare function isValidLedgerDate(date: unknown): date is string;
658
+ /** Finding categories the independent verifier can report. */
659
+ export type LedgerVerifyCode = 'unbalanced' | 'too-few-postings' | 'invalid-amount' | 'audit-chain-broken' | 'audit-hash-forged' | 'posted-hash-mismatch' | 'orphan-posting' | 'unknown-account' | 'replay-divergence' | 'entry-no-gap' | 'entry-no-duplicate' | 'entry-no-missing'
660
+ /** A `period.close`/`period.reopen` payload posting is not `pending`/unowned
661
+ * (LGR-12) — the writer always emits closing and reversal legs born
662
+ * `cleared: 'pending'`, `reconciliationId: null`, so a frozen payload saying
663
+ * otherwise was rewritten. This is what covers the workflow fields the
664
+ * period hash chain deliberately excludes. */
665
+ | 'closing-posting-forged'
666
+ /** An evidence manifest item on a posted/void entry is not `{filename,
667
+ * sha256, size}`-shaped (LGR-14) — the writer validates the shape at attach
668
+ * AND at post, so a malformed stored item was written out-of-band. The
669
+ * structural checks below cannot run on it, which is why it is a finding
670
+ * and not a skip. */
671
+ | 'evidence-manifest-invalid'
672
+ /** A posted/void entry's manifest names an asset the content-addressed store
673
+ * no longer holds (LGR-14). The manifest itself GC-protects its assets (the
674
+ * hash in the row's `properties` is what the GC's document scan keeps, and
675
+ * the tx-row `asset_refs` edge backs it up), and no API deletes asset rows —
676
+ * so a missing asset means the store was mutated underneath the ledger,
677
+ * never that the evidence "never existed": `post` re-resolves every item
678
+ * against the store inside the posting transaction. */
679
+ | 'evidence-asset-missing'
680
+ /** The stored bytes no longer hash to the manifest's SHA-256 (LGR-14) — a
681
+ * receipt was REPLACED in place by direct surgery on the `assets` row.
682
+ * Unreachable through any API (the id is derived from the bytes at upload;
683
+ * nothing updates `bytes`), so this re-hash is the only detector — with the
684
+ * row and every ledger hash untouched, no other check even looks. */
685
+ | 'evidence-asset-replaced'
686
+ /** The stored byte count differs from the manifest's `size` (LGR-14) while
687
+ * the hash still matches — the store's metadata was doctored (`size` is a
688
+ * cached column), or the manifest's size was rewritten in step with the row.
689
+ * Either way the manifest and the store disagree about the same bytes. */
690
+ | 'evidence-size-mismatch'
691
+ /**
692
+ * CURRENT-POLICY ADVISORY, not a tamper finding (LGR-14 F2): a posted/void
693
+ * entry has an EMPTY manifest while an account one of its legs touches
694
+ * CURRENTLY has `evidenceRequired`. This is what makes the toggle
695
+ * verifier-observable — without it, SQL-off the flag, post bare through the
696
+ * ordinary API, SQL it back on, and the book verified clean with
697
+ * `checkedEvidence: 0`: the feature's one traceless bypass.
698
+ *
699
+ * Read it as "the book does not satisfy TODAY'S policy", never "someone
700
+ * tampered": turning the flag on flags history — every bare entry posted
701
+ * before the requirement existed — and that is BY DESIGN (the operator asked
702
+ * "which posted entries can't answer for themselves under the current
703
+ * rules?", and pre-toggle history is exactly part of the answer). Its
704
+ * messages carry the `policy advisory` prefix so a report reader (and any
705
+ * alerting built on findings) can band it separately from the tamper codes.
706
+ *
707
+ * Carve-outs: reversals (`reverses !== null`) and closing entries — the
708
+ * post-time gate exempts both, so their bareness is never a policy breach.
709
+ * Since F1 a reversal CARRIES its original's manifest, so this carve-out in
710
+ * practice only shields reversals of bare legacy originals — and there the
711
+ * policy claim belongs to the ORIGINAL entry, which this same check flags
712
+ * (void entries are in scope precisely so a reversed bare entry stays
713
+ * visible).
714
+ */
715
+ | 'evidence-required-missing'
716
+ /**
717
+ * The LINEAR audit hash chain (`prev_hash`, migration 0021) fails to verify
718
+ * (LGR-15): some event's `prev_hash` is not the hash of its predecessor, or
719
+ * the genesis link is wrong. Distinct from `audit-chain-broken` (the
720
+ * per-entity before/after linkage): this is the whole-log tamper-evidence
721
+ * chain — an edited, reordered, or middle-deleted event whose perpetrator
722
+ * did not recompute every following link. Previously `verifyAuditChain`
723
+ * existed but had NO production caller; folding it into this report is what
724
+ * makes the documented check actually run at the route, the CLI, and the
725
+ * restore door.
726
+ */
727
+ | 'audit-prev-hash-broken';
728
+ /**
729
+ * The ADVISORY band of the finding-code union (LGR-14 Q3): codes that report
730
+ * "the book does not satisfy today's POLICY", never "storage was tampered
731
+ * with". Every other code is the tamper band. Keyed off the code union — not
732
+ * message text — because exit policies and alerting hang off this distinction:
733
+ * `--verify-ledger` exits 0 on an advisory-only report (still printed), so a
734
+ * healthy book that enables evidence-required over bare history does not turn
735
+ * every backup-verification script permanently red and teach operators to
736
+ * ignore the one alarm that matters. Any future advisory code MUST land here.
737
+ */
738
+ export declare const LEDGER_VERIFY_ADVISORY_CODES: readonly ["evidence-required-missing"];
739
+ /** Whether `code` is an advisory (current-policy) finding, not a tamper one. */
740
+ export declare function isLedgerVerifyAdvisory(code: LedgerVerifyCode): boolean;
741
+ /** One invariant violation the verifier found against raw storage. */
742
+ export interface LedgerVerifyFinding {
743
+ code: LedgerVerifyCode;
744
+ /** Human-readable, entity-id-bearing description of the violation. */
745
+ message: string;
746
+ /** The primary entity (transaction / posting / account / audit seq) at fault. */
747
+ entityId?: string;
748
+ }
749
+ /** The verifier's report: what was checked, and every finding (empty = clean). */
750
+ export interface LedgerVerifyReport {
751
+ /** False when the ledger has never been seeded — trivially clean. */
752
+ initialized: boolean;
753
+ checkedTransactions: number;
754
+ checkedPostings: number;
755
+ checkedAccounts: number;
756
+ checkedAuditEvents: number;
757
+ /** Period records checked against the audit stream (LGR-12). */
758
+ checkedPeriods: number;
759
+ /** Evidence manifest items re-checked against the asset store (LGR-14). */
760
+ checkedEvidence: number;
761
+ /** Reconciliation records checked against the audit stream (LGR-15). */
762
+ checkedReconciliations: number;
763
+ /**
764
+ * The linear `prev_hash` chain verdict (LGR-15) — `verifyLedgerAuditChain`
765
+ * over the full stream. Additive and absent on an uninitialized ledger. A
766
+ * broken chain also lands in `findings` as `audit-prev-hash-broken`, so
767
+ * severity-aware consumers (the CLI exit code) need no special casing.
768
+ */
769
+ auditChain?: LedgerAuditChainResult;
770
+ /** Empty = every invariant holds against raw storage. */
771
+ findings: LedgerVerifyFinding[];
772
+ }