@agentcomms/whatsapp 0.13.1 → 0.14.1

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.
package/dist/index.d.mts CHANGED
@@ -4277,7 +4277,25 @@ declare namespace external_d_exports {
4277
4277
  export { $brand, $input, $output, BRAND, CompileOptions, GlobalMeta, INVALID, Infer$1 as Infer, IssueData, JSONType, NEVER, $RefinementCtx as RefinementCtx, SafeExtendShape, Schema, TimePrecision, TypeOf, ZodAny, ZodArray, ZodBase64, ZodBase64URL, ZodBigInt, ZodBigIntFormat, ZodBoolean, ZodCIDRv4, ZodCIDRv6, ZodCUID, ZodCUID2, ZodCatch, ZodCodec, ZodCoercedBigInt, ZodCoercedBoolean, ZodCoercedDate, ZodCoercedNumber, ZodCoercedString, ZodCompileAsyncError, ZodCompileUnsupportedError, ZodCreditCard, ZodCustom, ZodCustomStringFormat, ZodDate, ZodDefault, ZodDiscriminatedUnion, ZodE164, ZodEmail, ZodEmoji, ZodEnum, ZodError, ZodErrorMap, ZodExactOptional, ZodFile, ZodFirstPartySchemaTypes, ZodFirstPartyTypeKind, ZodFlattenedError, ZodFloat32, ZodFloat64, ZodFormattedError, ZodFunction, ZodGUID, ZodIBAN, ZodIPv4, ZodIPv6, ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime, ZodInstanceOf, ZodInt, ZodInt32, ZodInt64, ZodIntersection, ZodIssue, ZodIssueCode, ZodJSONSchema, ZodJSONSchemaInternals, ZodJWT, ZodKSUID, ZodLazy, ZodLiteral, ZodMAC, ZodMap, ZodNaN, ZodNanoID, ZodNever, ZodNonOptional, ZodNull, ZodNullable, ZodNumber, ZodNumberFormat, ZodObject, ZodOptional, ZodPipe, ZodPrefault, ZodPreprocess, ZodPromise, ZodRawShape$1 as ZodRawShape, ZodReadonly, ZodRealError, ZodRecord, ZodSafeParseError, ZodSafeParseResult, ZodSafeParseSuccess, ZodSchema, ZodSet, ZodStandardSchemaWithJSON, ZodString, ZodStringFormat, ZodSuccess, ZodSymbol, ZodTemplateLiteral, ZodTransform, ZodTuple, ZodType, ZodTypeAny, ZodUInt32, ZodUInt64, ZodULID, ZodURL, ZodUUID, ZodUndefined, ZodUnion, ZodUnknown, ZodVoid, ZodXID, ZodXor, _ZodBigInt, _ZodBoolean, _ZodDate, _ZodNumber, _ZodString, _ZodType, _default, _function, any, array, base64, base64url, bigint$1 as bigint, boolean$1 as boolean, _catch as catch, check, cidrv4, cidrv6, clone, codec, coerce_d_exports as coerce, compile, config, index_d_exports$1 as core, creditCard, cuid, cuid2, currencyCode, custom, date$2 as date, decode, decodeAsync, deepPartial, describe, discriminatedUnion, e164, email, emoji, encode, encodeAsync, _endsWith as endsWith, _enum as enum, exactOptional, file, flattenError, float32, float64, formatError, fromJSONSchema, _function as function, getDiscriminatedOption, getErrorMap, globalRegistry, _gt as gt, _gte as gte, guid, hash, hex, hostname, httpUrl, iban, _includes as includes, output$1 as infer, inferFlattenedErrors, inferFormattedError, input, _instanceof as instanceof, int, int32, int64, intersection, invertCodec, ipv4, ipv6, iso_d_exports as iso, json, jwt, keyof, ksuid, lazy, _length as length, literal, index_d_exports$2 as locales, looseObject, looseRecord, _lowercase as lowercase, _lt as lt, _lte as lte, mac, map, _maxLength as maxLength, _maxSize as maxSize, memoizer, meta, _mime as mime, _minLength as minLength, _minSize as minSize, _multipleOf as multipleOf, nan, nanoid, nativeEnum, _negative as negative, never, _nonnegative as nonnegative, nonoptional, _nonpositive as nonpositive, _normalize as normalize, _null as null, nullable, nullish, number$1 as number, object, optional, output, _overwrite as overwrite, parse, parseAsync, partialRecord, pipe, _positive as positive, prefault, preprocess, prettifyError, promise, _properties as properties, _property as property, readonly, record, refine, _regex as regex, regexes_d_exports as regexes, registry, safeDecode, safeDecodeAsync, safeEncode, safeEncodeAsync, safeParse, safeParseAsync, set, setErrorMap, _size as size, _slugify as slugify, _startsWith as startsWith, strictObject, string$1 as string, stringFormat, stringbool, success, superRefine, symbol, templateLiteral, toJSONSchema, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, toZod, transform, treeifyError, _trim as trim, tuple, uint32, uint64, ulid, _undefined as undefined, union, unknown, _uppercase as uppercase, url, util_d_exports as util, uuid, uuidv4, uuidv6, uuidv7, validate, validateAsync, _void as void, withParser, xid, xor };
4278
4278
  }
4279
4279
  //#endregion
4280
- //#region ../core/dist/change-flow-i6vGyxUm.d.mts
4280
+ //#region ../core/dist/change-flow-mr6T-6nU.d.mts
4281
+ //#region src/config-hooks.d.ts
4282
+ /**
4283
+ * Test-only pauses inside `ConfigStore.convertToVersion3`, for the tests that prove a write racing the conversion is
4284
+ * neither lost nor let in between its scan and its write.
4285
+ *
4286
+ * Deliberately not exported from the package root, as `enableNamesMigrationForTests` is not (`release-gate.ts`): a
4287
+ * store is given them through its constructor under this symbol, which only a test importing this file by path can
4288
+ * name. No consumer of the package can reach it.
4289
+ */
4290
+ declare const CONVERSION_HOOKS: unique symbol;
4291
+ interface ConversionHooks {
4292
+ /** After the read before the lock, before the config lock is taken. */
4293
+ beforeLock?: (() => Promise<void> | void) | undefined;
4294
+ /** Under the lock, after the approvals were scanned and before the version-3 write is built. */
4295
+ afterScan?: (() => Promise<void> | void) | undefined;
4296
+ /** Under the lock, immediately before the version-3 write. */
4297
+ beforeWrite?: (() => Promise<void> | void) | undefined;
4298
+ }
4281
4299
  interface RegisteredServer {
4282
4300
  client: string;
4283
4301
  path: string;
@@ -4351,6 +4369,20 @@ interface ResolvedPaths {
4351
4369
  */
4352
4370
  downloadsDir: string;
4353
4371
  }
4372
+ type ErrorCode = "UNEXPECTED" | "APPROVAL_REQUIRED" | "APPROVAL_PENDING" | "APPROVAL_EXPIRED" | "APPROVAL_VOID" | "SEND_OUTCOME_UNKNOWN" | "SEND_REFUSED" | "POLICY_NEVER" | "RATE_CAPPED" | "UNSENDABLE_HTML" | "LOOSENING_REFUSED" | "UPDATE_REQUIRED" | "USAGE" | "CURSOR_MISMATCH" | "BAD_DATA" | "REPLY_INVALID" | "NOT_FOUND" | "PROVIDER_UNAVAILABLE" | "SECRET_STORE_UNAVAILABLE" | "TRANSIENT" | "KEYCHAIN_APPROVAL_PENDING" | "LOCK_TIMEOUT" | "AUTH_REQUIRED" | "SCOPE_MISSING" | "CONFIG";
4373
+ interface CommsErrorOptions {
4374
+ hint?: string;
4375
+ details?: Record<string, unknown>;
4376
+ cause?: unknown;
4377
+ }
4378
+ declare class CommsError extends Error {
4379
+ readonly code: ErrorCode;
4380
+ readonly hint: string | undefined;
4381
+ readonly details: Record<string, unknown> | undefined;
4382
+ constructor(code: ErrorCode, message: string, options?: CommsErrorOptions);
4383
+ get exitCode(): number;
4384
+ get retryable(): boolean;
4385
+ }
4354
4386
  //#endregion
4355
4387
  //#region src/cli-command.d.ts
4356
4388
  /**
@@ -4768,7 +4800,49 @@ interface ConfigV2 extends ConfigBody {
4768
4800
  */
4769
4801
  organisations?: Record<string, OrganisationRecord> | undefined;
4770
4802
  }
4771
- type Config$1 = ConfigV1 | ConfigV2;
4803
+ /** What a legacy drain records of one tracked record: still open, or what became of it. */
4804
+ type LegacyDrainOutcome = "revoked" | "expired" | "used" | "failed" | "sending" | "unknown";
4805
+ /**
4806
+ * The approvals an earlier release prepared that were still waiting, or already being sent, when this configuration
4807
+ * became version 3, and what has become of each (design 2026-10-05 §D1, "Old releases are locked out before the fence
4808
+ * is relied on").
4809
+ */
4810
+ interface LegacyDrain {
4811
+ /** When the conversion that opened the drain was written. */
4812
+ since: string;
4813
+ /**
4814
+ * Every tracked version-1 send record by id: `open` until it has an outcome — or `sending` from the start, for one an
4815
+ * earlier release was already sending when the conversion ran.
4816
+ */
4817
+ tracked: Record<string, "open" | LegacyDrainOutcome>;
4818
+ }
4819
+ /** What version 3 adds to a body of either naming. */
4820
+ interface Version3Fields {
4821
+ /** Each owner's send epoch by id; absent reads as 0. Raised by every write that turns its send policy to `never`. */
4822
+ sendEpochs?: Record<string, number> | undefined;
4823
+ /** Present while records from an earlier release are being retired. */
4824
+ legacyDrain?: LegacyDrain | undefined;
4825
+ }
4826
+ /** Version 3 over a version-1 body: one plain word per account. */
4827
+ interface ConfigV3Naming1 extends ConfigBody, Version3Fields {
4828
+ version: 3;
4829
+ naming: 1;
4830
+ }
4831
+ /** Version 3 over a version-2 body: `organisation/platform` names, and the names they replaced. */
4832
+ interface ConfigV3Naming2 extends Omit<ConfigV2, "version">, Version3Fields {
4833
+ version: 3;
4834
+ naming: 2;
4835
+ }
4836
+ /**
4837
+ * Version 3: the send epoch and the legacy drain (design 2026-10-05 §D1), over either naming — converted from version 1
4838
+ * or 2 without renaming anything, so its body keeps the rules of the version it came from.
4839
+ */
4840
+ type ConfigV3 = ConfigV3Naming1 | ConfigV3Naming2;
4841
+ type Config$1 = ConfigV1 | ConfigV2 | ConfigV3;
4842
+ /** A configuration whose accounts are named `organisation/platform`: version 2, or version 3 over it. */
4843
+ type NamedConfig = ConfigV2 | ConfigV3Naming2;
4844
+ /** A configuration whose accounts have one plain word each: version 1, or version 3 over it. */
4845
+ type PlainConfig = ConfigV1 | ConfigV3Naming1;
4772
4846
  /** The committed config returned by an update; process-only commit metadata is deliberately stored out of band. */
4773
4847
  type ConfigUpdateResult = Config$1;
4774
4848
  declare class ConfigStore {
@@ -4780,6 +4854,8 @@ declare class ConfigStore {
4780
4854
  */
4781
4855
  constructor(configDir: string, options?: {
4782
4856
  readonly handoffs?: CliHandoffs | undefined;
4857
+ /** Test-only pauses inside the version-3 conversion (`config-hooks.ts`); unreachable from the package root. */
4858
+ readonly [CONVERSION_HOOKS]?: ConversionHooks | undefined;
4783
4859
  });
4784
4860
  /** The current config; an empty one when the file does not exist yet. */
4785
4861
  load(): Promise<Config$1>;
@@ -4817,11 +4893,31 @@ declare class ConfigStore {
4817
4893
  *
4818
4894
  * The file it replaces is copied beside it first — see `backUpBeforeMigration` — and the copy's path returned.
4819
4895
  */
4820
- migrateNames(expected: string, rows: readonly RenamedAccount[], build: (current: ConfigV1) => ConfigV2): Promise<{
4896
+ migrateNames(expected: string, rows: readonly RenamedAccount[], build: (current: PlainConfig) => NamedConfig): Promise<{
4821
4897
  status: "migrated" | "already-migrated";
4822
- config: ConfigV2;
4898
+ config: NamedConfig;
4823
4899
  backup?: string;
4824
4900
  }>;
4901
+ /**
4902
+ * The one way a configuration becomes version 3 (design 2026-10-05 §D1, "Old releases are locked out before the
4903
+ * fence is relied on").
4904
+ *
4905
+ * Version 3 carries the send epoch, which 0.13 does not know, and 0.13 refuses to read it — so from this write on, no
4906
+ * 0.13 process can start a prepare, a claim, an approval or a policy change through this file. Nothing is renamed:
4907
+ * a version-1 body stays version 1's (`naming: 1`) until the names migration, which needs its own approval.
4908
+ *
4909
+ * Under the config lock, the file is read again — never from the cache, never from the read before the lock — and an
4910
+ * existing version 3 returned unchanged: a second converter that lost the race keeps the winner's epochs and drain.
4911
+ * `scan` runs under the same lock, so a record written while this waited for it is seen, and an update cannot land
4912
+ * between the scan and the write; the version-3 write carries `legacyDrain` only when the scan tracked something.
4913
+ * With no config file at all there is nothing to convert, and nothing is written.
4914
+ */
4915
+ convertToVersion3(scan: () => Promise<LegacyDrain["tracked"]>, options?: {
4916
+ now?: (() => Date) | undefined;
4917
+ }): Promise<{
4918
+ status: "converted" | "already" | "empty";
4919
+ config: Config$1;
4920
+ }>;
4825
4921
  }
4826
4922
  /** One account's rename, as the migration planned it. */
4827
4923
  interface RenamedAccount {
@@ -4883,6 +4979,373 @@ interface LooseningConsent {
4883
4979
  */
4884
4980
  changes?: readonly Loosening[] | undefined;
4885
4981
  }
4982
+ //#endregion
4983
+ //#region src/approval-binding.d.ts
4984
+ /**
4985
+ * What a version-2 approval binds, defined once (design 2026-10-05 §D1, "Digest integrity").
4986
+ *
4987
+ * A record stores two digests. `contentDigest` is the outward content or change, computed as before: the message, the
4988
+ * change, the download. `bindingDigest` is this module's: the content digest together with the record's route, its
4989
+ * lifetimes and its own identity — who it belongs to, which draft, which channel, which send epoch. Every one of those
4990
+ * is a field a claim or a report acts on, so a record whose binding no longer recomputes from its own fields has been
4991
+ * altered since it was made, and nothing it says can be trusted.
4992
+ *
4993
+ * There is no second copy of anything: the binding is recomputed from the stored fields alone, never stored apart
4994
+ * from them as a key.
4995
+ */
4996
+ /**
4997
+ * How an approval is decided, fixed when it is made: `chat` — a yes in the conversation is enough — or `confirm` — a
4998
+ * person outside the chat decides. A send's route is the stricter of its live send policy and any escalation; a
4999
+ * change's is the live change policy. It never changes when the configuration does.
5000
+ */
5001
+ type ApprovalRoute = "chat" | "confirm";
5002
+ /**
5003
+ * What a record's `inboxId` names. `owner` — a mailbox or account that existed when it was made (every send and
5004
+ * download, and a change to one). `prospective` — a change that creates its owner, which has no id yet. `global` — a
5005
+ * change to no owner at all, stored with `inboxId: ''`.
5006
+ */
5007
+ type OwnerScope = "owner" | "prospective" | "global";
5008
+ /**
5009
+ * The file operations a lock is made of, one method each, so a test can count and interrupt every one of them: the
5010
+ * create that is the attempt, the read that checks a holder, the touch that renews, the unlink that releases.
5011
+ * `withFileLock` uses the real ones; `tryFileLock` takes another for a caller whose every step is counted.
5012
+ */
5013
+ interface LockIo {
5014
+ /** Creates `path` exclusively (`wx`, 0600) holding `body`: the one attempt to take the lock. */
5015
+ create(path: string, body: string): Promise<void>;
5016
+ /** The lock file's text: a holder's body. */
5017
+ read(path: string): Promise<string>;
5018
+ /** The lock file's modification time, in milliseconds: where a renewal shows. */
5019
+ mtime(path: string): Promise<number>;
5020
+ /** Sets the lock file's times to now: a renewal. */
5021
+ touch(path: string): Promise<void>;
5022
+ rename(from: string, to: string): Promise<void>;
5023
+ link(from: string, to: string): Promise<void>;
5024
+ /** Removes a lock file, or one moved aside; a missing one is no error. */
5025
+ remove(path: string): Promise<void>;
5026
+ }
5027
+ //#endregion
5028
+ //#region src/approval-io.d.ts
5029
+ /**
5030
+ * The file operations approval maintenance and the unsent report are made of (design 2026-10-05 §D9), one method
5031
+ * each, so that a test can count every one of them — every directory read, metadata stat, record open, lock try,
5032
+ * renewal, ownership check and unlink — and none is hidden behind a helper. The store passes the real ones unless it
5033
+ * was opened with others.
5034
+ *
5035
+ * Only these two passes go through it: every other read and write of the store is as it was.
5036
+ */
5037
+ interface ApprovalIo {
5038
+ /** One enumeration of a directory: its entries' names. */
5039
+ readdir(directory: string): Promise<string[]>;
5040
+ /** One metadata stat: the modification time, in milliseconds. */
5041
+ stat(path: string): Promise<{
5042
+ readonly mtimeMs: number;
5043
+ }>;
5044
+ /** One open of a file's content, as text. */
5045
+ readText(path: string): Promise<string>;
5046
+ /** Replaces a file atomically — a temporary file, flushed, renamed — and, when `durable`, syncs its directory too. */
5047
+ writeAtomic(path: string, text: string, options?: {
5048
+ durable?: boolean;
5049
+ }): Promise<void>;
5050
+ /** One unlink of an approval artifact: a record or its claim marker. A missing one rejects with `ENOENT`. */
5051
+ unlink(path: string): Promise<void>;
5052
+ /** The lock files' own operations (`tryFileLock`). */
5053
+ readonly lock: LockIo;
5054
+ }
5055
+ /** A version-1 record, as 0.13.0 wrote it: a send has no `kind`, and one `digest` stands for the content. */
5056
+ interface LegacyApprovalRecord {
5057
+ approvalId: string;
5058
+ /** Absent on a send. */
5059
+ kind?: ApprovalKind | undefined;
5060
+ digestVersion: 1;
5061
+ inboxId: string;
5062
+ inboxSub?: string | undefined;
5063
+ draftId: string;
5064
+ draftMessageId: string;
5065
+ digest: string;
5066
+ approvedDigest?: string | undefined;
5067
+ approvedVia?: ApprovalChannel | undefined;
5068
+ policy: SendPolicy;
5069
+ requiredPolicy: SendPolicy;
5070
+ riskFlags: string[];
5071
+ expect: Expectation;
5072
+ challengeHash?: string | undefined;
5073
+ challengeAttempts: number;
5074
+ state: ApprovalState;
5075
+ createdAt: string;
5076
+ expiresAt: string;
5077
+ updatedAt: string;
5078
+ sentMessageId?: string | undefined;
5079
+ reason?: string | undefined;
5080
+ change?: ChangeBinding | undefined;
5081
+ download?: DownloadBinding | undefined;
5082
+ }
5083
+ /**
5084
+ * A version-1 record as every surface shows it: `{ approvalId, kind, state, legacy: true, createdAt, expiresAt }` and
5085
+ * the summary a version-2 record shows — its owner, what it was for, and which channel made it when that can be told.
5086
+ * Never claimable, and never given a version-2 timestamp it does not hold.
5087
+ */
5088
+ interface LegacyView {
5089
+ readonly approvalId: string;
5090
+ readonly kind: ApprovalKind;
5091
+ /** Derived as 0.13.0 derived it (`deriveLegacyV1State`). */
5092
+ readonly state: ApprovalState;
5093
+ readonly legacy: true;
5094
+ readonly createdAt: string;
5095
+ readonly expiresAt: string;
5096
+ /** The stored owner, trusted as stored: a version-1 record has no binding to check it against. */
5097
+ readonly inboxId: string;
5098
+ readonly ownerScope: "owner" | "global";
5099
+ /**
5100
+ * The channel that made it, when only one could have: an `ibx_` owner is a Gmail mailbox, an `acc_` owner the
5101
+ * stored platform of an account that still exists. Null when nothing says — it then takes part in no channel's
5102
+ * grouping and gets only generic wording.
5103
+ */
5104
+ readonly channel: string | null;
5105
+ readonly expect: Expectation;
5106
+ readonly reason?: string | undefined;
5107
+ readonly sentMessageId?: string | undefined;
5108
+ }
5109
+ //#endregion
5110
+ //#region src/audit.d.ts
5111
+ /**
5112
+ * Every mailbox write leaves one line here: what was done, to which inbox, with which ids and outcome. Never message
5113
+ * bodies, never secrets — recipient *domains* only, so the log can be shared when asking for help.
5114
+ */
5115
+ interface AuditRecord {
5116
+ at: string;
5117
+ /** Immutable inbox id; `alias` is the label at the time. */
5118
+ inboxId: string;
5119
+ alias?: string;
5120
+ operation: string;
5121
+ /** `started` marks the first half of a change recorded before it is made; a later record for the same ids ends it. */
5122
+ outcome: "ok" | "refused" | "failed" | "started";
5123
+ ids?: Record<string, string | string[] | CondensedIds>;
5124
+ recipientDomains?: string[];
5125
+ /**
5126
+ * Full canonical recipient addresses. Only a send writes these: for every other operation the domains are enough,
5127
+ * and storing less is better — but when mail has actually left, "who did it go to" is the first question anyone
5128
+ * asks afterwards, and a domain does not answer it.
5129
+ */
5130
+ recipients?: string[];
5131
+ approvalId?: string;
5132
+ reason?: string;
5133
+ surface?: "cli" | "mcp";
5134
+ /**
5135
+ * The policy that decided how this was approved. Written by change approvals, where it is the whole question
5136
+ * afterwards: a loosening a person typed a code for and one an agent claimed after a yes in chat look the same in
5137
+ * the configuration, and only this line tells them apart.
5138
+ */
5139
+ policy?: SendPolicy;
5140
+ /**
5141
+ * What daily retention deleted (`operation: 'approval.retained'`, design 2026-10-05 §D9): the approval's kind, its
5142
+ * final state, when it finished and any provider id — never its content or a recipient. Appended durably before the
5143
+ * record is unlinked, so the audit log keeps the terminal history the approval directory no longer does.
5144
+ */
5145
+ retained?: RetainedApproval;
5146
+ }
5147
+ /** Large id lists are condensed so every audit line stays small enough to be appended atomically. */
5148
+ interface CondensedIds {
5149
+ count: number;
5150
+ sha256: string;
5151
+ first: string[];
5152
+ }
5153
+ declare class AuditLog {
5154
+ #private;
5155
+ readonly directory: string;
5156
+ constructor(stateDir: string, now?: () => Date);
5157
+ append(record: Omit<AuditRecord, "at"> & {
5158
+ at?: string;
5159
+ }, options?: {
5160
+ durable?: boolean;
5161
+ }): Promise<AuditRecord>;
5162
+ /** The most recent records, newest last, optionally filtered by inbox and a lower time bound. */
5163
+ tail(options?: {
5164
+ limit?: number;
5165
+ inbox?: string;
5166
+ since?: string;
5167
+ }): Promise<AuditRecord[]>;
5168
+ }
5169
+ /** The step of a batch that failed. */
5170
+ type MaintenanceStep = "lock" | "state" | "enumerate" | "stat" | "read" | "write" | "append" | "claim-unlink" | "record-unlink" | "lock-lost" | "cursor";
5171
+ /** A failure a batch met: which step, for which record, and the error's code — never anything a record holds. */
5172
+ interface MaintenanceError {
5173
+ readonly step: MaintenanceStep;
5174
+ readonly approvalId?: string | undefined;
5175
+ readonly code: string;
5176
+ }
5177
+ /**
5178
+ * What one call of `ensurePruned` did. `attempted` — it ran a batch; when it did not, `skipped` says why: not due
5179
+ * (a batch ran less than a day ago), busy (another process holds the maintenance lock), or no records (there is no
5180
+ * approvals directory at all). `processed` counts the slots whose record work ran to its end, `pruned` the records
5181
+ * deleted, `skippedBusy` the records whose lock was held elsewhere and were left. `complete` — the batch reached the end
5182
+ * of the directory inside both bounds; hitting 200 slots or the deadline, or losing the lock, leaves it false.
5183
+ */
5184
+ interface MaintenanceStatus {
5185
+ readonly attempted: boolean;
5186
+ readonly processed: number;
5187
+ readonly pruned: number;
5188
+ readonly skippedBusy: number;
5189
+ readonly complete: boolean;
5190
+ readonly errors: readonly MaintenanceError[];
5191
+ readonly skipped?: "not-due" | "busy" | "no-records" | undefined;
5192
+ }
5193
+ /** What a batch reports of a record it is about to delete: never a body, an address, or anything a sender wrote. */
5194
+ interface RetainedApproval {
5195
+ readonly kind: ApprovalKind;
5196
+ readonly state: ApprovalState;
5197
+ /** When it finished: its validated terminal time, or a legacy used record's safe `updatedAt`. */
5198
+ readonly finishedAt: string;
5199
+ /** A used send's provider id, when it has a non-empty one. */
5200
+ readonly providerId?: string | undefined;
5201
+ /** A record an earlier release wrote. */
5202
+ readonly legacy?: true | undefined;
5203
+ }
5204
+ interface MaintenanceOptions {
5205
+ /**
5206
+ * When no further step may start, on the store's clock, in milliseconds: five seconds from the call when left out.
5207
+ * A report passes the one deadline it shares with its own reads.
5208
+ */
5209
+ readonly deadline?: number | undefined;
5210
+ }
5211
+ //#endregion
5212
+ //#region src/approval-validate.d.ts
5213
+ /**
5214
+ * The integrity check of a version-2 approval record (design 2026-10-05 §D1, "Digest integrity" and "Version-2
5215
+ * timestamps fail closed"). Pure: it reads the record and the id its file is named by, and decides.
5216
+ *
5217
+ * A record either passes, or is `corrupt` for one fixed reason — never a sentence built from the record, so nothing a
5218
+ * file holds is ever echoed — and of one of two attribution classes. `verified`: its `bindingDigest` recomputes, so its
5219
+ * owner and identity can be trusted and it is corrupt for another reason (a state, a timestamp, its evidence).
5220
+ * `unverifiable`: its binding is missing, malformed or does not match, or it sits in another record's file, so nothing
5221
+ * it says about whom it belongs to can be trusted.
5222
+ *
5223
+ * Nothing here is wired into a read yet; the store's decoder does that.
5224
+ */
5225
+ /** Why a version-2 record is corrupt: a fixed category, never the record's own words. */
5226
+ type IntegrityReason = "file-name-mismatch" | "binding-missing" | "binding-malformed" | "binding-mismatch" | "wrong-shape" | "digest-version" | "content-digest-malformed" | "content-digest-mismatch" | "identity-contradiction" | "listing-mismatch" | "state-impossible" | "lifetime-mismatch" | "timestamp-invalid" | "timestamp-missing" | "timestamp-misplaced" | "timestamp-misordered" | "sent-id-missing" | "evidence-missing" | "evidence-invalid" | "evidence-contradictory";
5227
+ type Attribution = "verified" | "unverifiable";
5228
+ //#endregion
5229
+ //#region src/approval-stored.d.ts
5230
+ /**
5231
+ * Every approval file, read one way (design 2026-10-05 §D2, "Legacy version-1 records, one decoder everywhere" and
5232
+ * "Two classes of parseable corrupt record").
5233
+ *
5234
+ * A file is one of four things, and every read says which:
5235
+ *
5236
+ * - `v2` — a version-2 record that passed `validateV2`: the only form anything may act on;
5237
+ * - `legacy` — a structurally valid version-1 record, read by its own release's rules: reported, never claimed;
5238
+ * - `corrupt` — a parseable version-2 record that failed its integrity check, attributable to its owner or not;
5239
+ * - `unreadable` — a file that cannot safely be read at all: shown only as a stub naming its id (from the file name)
5240
+ * and a fixed reason, never anything the file holds.
5241
+ *
5242
+ * Nothing is silently omitted, and the decoder writes nothing.
5243
+ */
5244
+ /** Why a file cannot safely be read: a fixed category, never the file's own bytes. */
5245
+ type UnreadableReason = "invalid-json" | "truncated" | "missing-ownership" | "missing-kind" | "wrong-shape" | "file-name-mismatch";
5246
+ /** What may be said of a record that cannot be trusted: its id, that it is corrupt, and why — nothing else. */
5247
+ interface CorruptStub {
5248
+ readonly approvalId: string;
5249
+ readonly state: "corrupt";
5250
+ readonly reason: UnreadableReason | IntegrityReason;
5251
+ }
5252
+ /** What an attribution-verified corrupt record may still show its owner: the fields its binding was made over. */
5253
+ interface SafeFields {
5254
+ readonly kind: ApprovalKind;
5255
+ readonly channel: string;
5256
+ readonly ownerScope: OwnerScope;
5257
+ readonly inboxId: string;
5258
+ readonly inboxSub?: string | undefined;
5259
+ readonly draftId: string;
5260
+ readonly draftMessageId: string;
5261
+ readonly expect: Expectation;
5262
+ }
5263
+ type StoredApproval = {
5264
+ readonly form: "v2";
5265
+ readonly record: ApprovalRecord;
5266
+ } | {
5267
+ readonly form: "legacy";
5268
+ readonly view: LegacyView;
5269
+ readonly record: LegacyApprovalRecord;
5270
+ } | {
5271
+ readonly form: "corrupt";
5272
+ readonly approvalId: string;
5273
+ readonly reason: IntegrityReason;
5274
+ readonly attribution: Attribution;
5275
+ /** Only when attribution is verified. */
5276
+ readonly safe: SafeFields | null;
5277
+ } | {
5278
+ readonly form: "unreadable";
5279
+ readonly stub: CorruptStub;
5280
+ };
5281
+ //#endregion
5282
+ //#region src/approval-outcome.d.ts
5283
+ /**
5284
+ * One locked classification of every record into an outcome (design 2026-10-05 §D2).
5285
+ *
5286
+ * `approvalOutcome` says what a record is now — its state, whether it can be claimed, and what an action on it would be
5287
+ * told — from the record as it was read under its own lock and the configuration read in that same lock (`LiveGate`).
5288
+ * Nothing here reads a file or a clock of its own, and nothing here writes: the store persists what the outcome
5289
+ * derives, and only from inside the lock it was classified under.
5290
+ */
5291
+ /** What is being done with the record: an approval, a claim, a wait, a revoke, or a look. */
5292
+ type OutcomeAction = "approve" | "claim" | "wait" | "revoke" | "inspect";
5293
+ /** A record's state as anyone may be told it: a download's answered question says `answered`. */
5294
+ type PublicApprovalState = ApprovalState | "corrupt" | "answered";
5295
+ /**
5296
+ * Where an approval stands, as every surface may show it (design 2026-10-05 §D8): its id, kind, channel, state and
5297
+ * whether it can be claimed, with the timestamps its state carries. Never a challenge hash, and nothing a sender wrote.
5298
+ */
5299
+ interface ApprovalObject {
5300
+ readonly id: string;
5301
+ readonly kind: ApprovalKind | null;
5302
+ readonly channel: string | null;
5303
+ readonly state: PublicApprovalState;
5304
+ readonly claimable: boolean;
5305
+ readonly route?: ApprovalRoute | undefined;
5306
+ readonly ownerRemoved?: true | undefined;
5307
+ /** A record from an earlier release, read by its own rules: shown, never claimed. */
5308
+ readonly legacy?: true | undefined;
5309
+ readonly reason?: string | undefined;
5310
+ readonly createdAt?: string | undefined;
5311
+ readonly expiresAt?: string | undefined;
5312
+ readonly approvedAt?: string | undefined;
5313
+ readonly usableUntil?: string | undefined;
5314
+ readonly sendingAt?: string | undefined;
5315
+ readonly sendingHeartbeatAt?: string | undefined;
5316
+ /** When a `sending` record reads `unknown` (`unknownAtOf`): derived, never stored. Also on an `unknown` one. */
5317
+ readonly unknownAt?: string | undefined;
5318
+ readonly usedAt?: string | undefined;
5319
+ readonly sentAt?: string | undefined;
5320
+ readonly sentMessageId?: string | undefined;
5321
+ readonly failedAt?: string | undefined;
5322
+ readonly revokedAt?: string | undefined;
5323
+ readonly expiredAt?: string | undefined;
5324
+ }
5325
+ /** What an action on a record is, now. */
5326
+ interface ApprovalOutcome {
5327
+ readonly state: PublicApprovalState;
5328
+ readonly claimable: boolean;
5329
+ readonly ownerRemoved?: true | undefined;
5330
+ readonly reason?: string | undefined;
5331
+ /**
5332
+ * A version-2 record as this classification reads it: with a revocation it derived applied (an owner removed, a
5333
+ * stale send epoch, or — for a claim or an approval — a live `never`). Null for every other form.
5334
+ */
5335
+ readonly record: ApprovalRecord | null;
5336
+ /** Whether `record` carries a revocation the classification derived, for a locked action to persist. */
5337
+ readonly revokes: boolean;
5338
+ /** Where it stands, as D8 shows it (`publicApproval`). */
5339
+ readonly approval: ApprovalObject;
5340
+ /** What the action is refused with, when it is refused by the record's state alone. */
5341
+ readonly error?: CommsError | undefined;
5342
+ }
5343
+ /**
5344
+ * What became of a used send, in the words of the surface that says it, from the time the provider accepted it (D2's
5345
+ * used-send row): Gmail and Slack, where acceptance is sending, say it was "sent at …"; Resend's own surfaces that it
5346
+ * was "accepted by Resend at …"; core's generic surfaces "accepted by <provider> at …".
5347
+ */
5348
+ type UsedSaid = (usedAt: string) => string;
4886
5349
  /**
4887
5350
  * Why a saved name has {@link DOWNLOAD_SUFFIX} after it: a name tools read or run on their own (`auto-read`), an
4888
5351
  * extension that is not one of the inert ones (`type`), or no extension at all (`no-extension`).
@@ -5032,19 +5495,54 @@ interface Expectation {
5032
5495
  }
5033
5496
  interface ApprovalRecord {
5034
5497
  approvalId: string;
5035
- /** Absent on a send, so a send's record is byte for byte what it was before change approvals existed. */
5036
- kind?: ApprovalKind | undefined;
5498
+ /** What it permits. Every version-2 record says, a send included; only a version-1 send was written without one. */
5499
+ kind: ApprovalKind;
5037
5500
  digestVersion: number;
5501
+ /** The manifest channel that made it (`gmail`, `slack`, `resend`, `whatsapp`, `core`), checked at creation. Bound. */
5502
+ channel: string;
5503
+ /** What `inboxId` names: an owner that existed, one the change creates, or none at all. Bound. */
5504
+ ownerScope: OwnerScope;
5505
+ /** A send's or a change's route, fixed at creation: a yes in the chat, or a person outside it. Bound. */
5506
+ route?: ApprovalRoute | undefined;
5507
+ /** How long it stays pending, from creation. Bound. */
5508
+ pendingMs: number;
5509
+ /** A send's or a change's lifetime once approved. Bound. */
5510
+ approvedMs?: number | undefined;
5038
5511
  /** For a change: the id of the inbox or account it is about, or empty for one to the whole configuration. */
5039
5512
  inboxId: string;
5040
5513
  inboxSub?: string | undefined;
5041
5514
  draftId: string;
5042
5515
  /** Changes on every save of the draft; binding to it detects any edit, even one that restores identical content. */
5043
5516
  draftMessageId: string;
5044
- digest: string;
5045
- /** The digest the human was actually shown when approving through a confirm channel. */
5517
+ /** The outward content or change: the message, change or download digest. */
5518
+ contentDigest: string;
5519
+ /** The SHA-256 of everything the record binds (`bindingDigestOf`), recomputed from its own fields on every read. */
5520
+ bindingDigest: string;
5521
+ /** On a send: the owner's send epoch as it was read at prepare. Bound. */
5522
+ sendEpoch?: number | undefined;
5523
+ /**
5524
+ * A download's answer, bound: SHA-256 of canonical `{ bindingDigest, answer }`, written when a person answered at a
5525
+ * terminal or in a trusted form. Never on a send or a change, which carry `approvedBindingDigest` instead.
5526
+ */
5046
5527
  approvedDigest?: string | undefined;
5528
+ /** Where a person approved, or answered: the terminal, or a trusted client's form. Absent on a chat-route claim. */
5047
5529
  approvedVia?: ApprovalChannel | undefined;
5530
+ /** A send's or a change's approval: when, and the binding the person approved, which must still be the record's. */
5531
+ approvedAt?: string | undefined;
5532
+ /** `approvedAt + approvedMs`: until when an approved send or change may be claimed. */
5533
+ usableUntil?: string | undefined;
5534
+ approvedBindingDigest?: string | undefined;
5535
+ /** When a send was claimed for sending. Kept on every state after it. */
5536
+ sendingAt?: string | undefined;
5537
+ /** The sending claimant's latest lease renewal, when it has made one. */
5538
+ sendingHeartbeatAt?: string | undefined;
5539
+ /** When a record was used: for a send, the moment the provider accepted it, and equal to `sentAt`. */
5540
+ usedAt?: string | undefined;
5541
+ sentAt?: string | undefined;
5542
+ failedAt?: string | undefined;
5543
+ revokedAt?: string | undefined;
5544
+ /** The boundary that applied when it expired — or, on a `clock-anomaly` expiry, the time the clock was seen. */
5545
+ expiredAt?: string | undefined;
5048
5546
  /** Live policy at prepare time, for display and audit. */
5049
5547
  policy: SendPolicy;
5050
5548
  /** `confirm` when risk escalation raised this send. The effective policy is the stricter of this and the live one. */
@@ -5060,23 +5558,29 @@ interface ApprovalRecord {
5060
5558
  updatedAt: string;
5061
5559
  sentMessageId?: string | undefined;
5062
5560
  reason?: string | undefined;
5063
- /** For a change: exactly what it permits. `digest` is `changeDigest` of this. */
5561
+ /** For a change: exactly what it permits. `contentDigest` is `changeDigest` of this. */
5064
5562
  change?: ChangeBinding | undefined;
5065
- /** For a download's question: what it asked about, and the folders it offered. `digest` is `downloadDigest`. */
5563
+ /** For a download's question: what it asked about, and the folders it offered. `contentDigest` is `downloadDigest`. */
5066
5564
  download?: DownloadBinding | undefined;
5067
5565
  }
5068
5566
  interface CreateApprovalInput {
5567
+ /** The manifest channel preparing the send. A channel this release does not know is refused. */
5568
+ channel: string;
5069
5569
  inboxId: string;
5070
5570
  inboxSub?: string | undefined;
5071
5571
  draftId: string;
5072
5572
  draftMessageId: string;
5073
- digest: string;
5573
+ contentDigest: string;
5574
+ /** The owner's send epoch, read from the configuration at prepare (`sendEpochOf`). */
5575
+ sendEpoch: number;
5074
5576
  policy: SendPolicy;
5075
5577
  requiredPolicy: SendPolicy;
5076
5578
  riskFlags: string[];
5077
5579
  expect: Expectation;
5078
5580
  }
5079
5581
  interface CreateChangeApprovalInput {
5582
+ /** The manifest channel of the surface preparing the change: `core` for core's own. */
5583
+ channel: string;
5080
5584
  change: ChangeBinding;
5081
5585
  /** The change policy in force before the change: it decides how the change is approved. */
5082
5586
  policy: ChangePolicy;
@@ -5089,7 +5593,7 @@ interface LiveChange {
5089
5593
  /** What the caller observed in the live draft at the moment of a transition. */
5090
5594
  interface LiveDraft {
5091
5595
  draftMessageId: string;
5092
- digest: string;
5596
+ contentDigest: string;
5093
5597
  }
5094
5598
  /** What the product making a claim tells the store about itself. */
5095
5599
  interface ClaimOptions {
@@ -5109,6 +5613,11 @@ interface ClaimOptions {
5109
5613
  * names no product at all rather than the wrong one.
5110
5614
  */
5111
5615
  pendingHint?: string | undefined;
5616
+ /**
5617
+ * How a used send is said in the refusal of a claim of it — the surface's own words, as `pendingHint` is (design
5618
+ * 2026-10-05 §D2's used-send row): "sent at …" when left out, as Gmail and Slack say it.
5619
+ */
5620
+ usedSaid?: UsedSaid | undefined;
5112
5621
  /**
5113
5622
  * The caller's cancellation — an MCP request's signal — asked under the record's lock, immediately before the claim
5114
5623
  * would change the record.
@@ -5121,19 +5630,124 @@ interface ClaimOptions {
5121
5630
  */
5122
5631
  signal?: AbortSignal | undefined;
5123
5632
  }
5633
+ /** What a send's claim gives its claimant: the record, and the private token only the claimant holds (`heartbeat`, `fence`, `complete`). */
5634
+ interface SendClaim {
5635
+ readonly record: ApprovalRecord;
5636
+ /**
5637
+ * 16 random bytes, hex: kept only in the claim's `O_EXCL` marker beside the record, and handed only to the call
5638
+ * that claimed. Never in a record, a view, an approval object, an audit row, an error or a result.
5639
+ */
5640
+ readonly claimToken: string;
5641
+ /** Where the approval stands now that it is claimed (design 2026-10-05 §D8): `sending`, never claimable. */
5642
+ readonly approval: ApprovalObject;
5643
+ }
5644
+ /** A send's outcome, as only its claimant records it. */
5645
+ type SendOutcome = {
5646
+ readonly sentMessageId: string;
5647
+ } | {
5648
+ readonly error: string;
5649
+ };
5650
+ /**
5651
+ * Whose decision a revoke carries, which decides what it may do to a record from an earlier release (version 1).
5652
+ *
5653
+ * `person` — a person cancelled it, from a tool, a command or a terminal. `lifecycle` — the owner it belongs to is being
5654
+ * removed, which a person approved. Either retires a version-1 record in its own shape (`revokeLegacy`), so a running
5655
+ * earlier release can no longer claim it. `integrity` — this release found the record does not match what it binds:
5656
+ * that never rewrites a version-1 record, which this release cannot judge by its own rules, and gets the version
5657
+ * refusal instead. For a version-2 record all three revoke alike.
5658
+ */
5659
+ type RevokeDisposition = "person" | "lifecycle" | "integrity";
5660
+ /**
5661
+ * What a caller takes an id to be: an approval of `kind` (any kind when left out); on a channel's own surface that is
5662
+ * no one owner's — its terminal approval — one of `channel`'s, the manifest channel that prepared it; and on a surface
5663
+ * pinned to one mailbox or account, or a claim made for one, owned by `owner`. Anything else is the one `NOT_FOUND`
5664
+ * (`approvalNotFound`), checked before the record's state is looked at: another channel's send given to Gmail's
5665
+ * `approve` is not found there, as one nobody prepared is not (design 2026-10-05 §D2).
5666
+ */
5667
+ interface ApprovalExpectation {
5668
+ readonly kind?: ApprovalKind | undefined;
5669
+ readonly channel?: string | undefined;
5670
+ readonly owner?: string | undefined;
5671
+ }
5672
+ /** How the store is opened. */
5673
+ interface ApprovalStoreOptions {
5674
+ now?: () => Date;
5675
+ handoffs?: CliHandoffs | undefined;
5676
+ /**
5677
+ * The configuration, read afresh: the one thing a read of the store consults beyond the files themselves — to
5678
+ * attribute a version-1 record whose owner is an account to that account's platform, and for every locked
5679
+ * transition, the live gate it classifies by (`liveGateOf`): the owner, its policies and its send epoch. Read once
5680
+ * per `get`, once per `list`, and once per locked transition after its lock is held, so one call decodes and
5681
+ * classifies against one snapshot. Its errors propagate: a configuration that cannot be read is never taken for none.
5682
+ * Resolving `null`, or left out, is "no configuration": a version-1 account record is then unattributable, nothing is
5683
+ * ever claimable, and a claim, an approval or an answer is refused with `CONFIG`.
5684
+ */
5685
+ loadConfig?: (() => Promise<Config$1 | null>) | undefined;
5686
+ /**
5687
+ * The audit log daily retention records each deletion in (`approval.retained`, design 2026-10-05 §D9): the state
5688
+ * directory's own when left out. `openCore` passes the one it opens.
5689
+ */
5690
+ audit?: Pick<AuditLog, "append"> | undefined;
5691
+ /**
5692
+ * The file operations daily maintenance and the unsent report go through — the real ones when left out; a test's to
5693
+ * count and interrupt every step. Every other read and write of the store is unaffected.
5694
+ */
5695
+ io?: ApprovalIo | undefined;
5696
+ }
5124
5697
  declare class ApprovalStore {
5125
5698
  #private;
5126
5699
  readonly directory: string;
5127
- constructor(stateDir: string, options?: {
5128
- now?: () => Date;
5129
- ttlMs?: number;
5130
- downloadTtlMs?: number;
5131
- handoffs?: CliHandoffs | undefined;
5132
- });
5700
+ constructor(stateDir: string, options?: ApprovalStoreOptions);
5701
+ /**
5702
+ * Daily retention (design 2026-10-05 §D9): once a day per state directory, a bounded batch deletes valid finished
5703
+ * records ninety days past their finish. Awaited before every creation, list, status and wait — with its own
5704
+ * five-second budget — and before a report, on the report's shared deadline. Never a gate: whatever it finds, it
5705
+ * returns its status and the caller goes on.
5706
+ */
5707
+ ensurePruned(options?: MaintenanceOptions): Promise<MaintenanceStatus>;
5133
5708
  create(input: CreateApprovalInput): Promise<ApprovalRecord>;
5134
- get(approvalId: string): Promise<ApprovalRecord | null>;
5709
+ /**
5710
+ * One approval, in whichever of its four forms it is (`StoredApproval`), read against one configuration snapshot —
5711
+ * or null when there is none. Nothing is written.
5712
+ */
5713
+ get(approvalId: string): Promise<StoredApproval | null>;
5714
+ /**
5715
+ * A look at one approval, under its lock: the record as it stands and its classification for `expect` — the same
5716
+ * `NOT_FOUND` as every action for an id that is not what the caller expects, before anything of the record is
5717
+ * classified. It writes only what reading derives (an expiry, a dead send's `unknown`), never a revocation: an owner
5718
+ * removed or a stale epoch is shown as revoked here, and written by the next action.
5719
+ *
5720
+ * `action` is for a surface about to act on what it reads — a terminal about to show a preview, an execute about to
5721
+ * read its draft — classified for that action (`approve`, `claim`, `revoke`), so the refusal is the one the action
5722
+ * would get. A revocation the classification derives for it is written then, as the action itself would write it:
5723
+ * the first locked action persists it, and this is that action's first step (design 2026-10-05 §D2).
5724
+ */
5725
+ inspect(approvalId: string, expect?: ApprovalExpectation, options?: {
5726
+ action?: OutcomeAction | undefined;
5727
+ usedSaid?: UsedSaid | undefined;
5728
+ }): Promise<{
5729
+ stored: StoredApproval;
5730
+ outcome: ApprovalOutcome;
5731
+ }>;
5732
+ /**
5733
+ * D8's object for a record a call has just written, claimed or finished — what its result reports. Classified as a
5734
+ * look at it would be, against a fresh read of the configuration, without the lock and writing nothing: what decided
5735
+ * was the transition that wrote it. A configuration that cannot be read leaves only what the record says (nothing
5736
+ * claimable): a report never fails the call it reports on, least of all one that has already sent.
5737
+ */
5738
+ approvalOf(record: ApprovalRecord): Promise<ApprovalObject>;
5739
+ /**
5740
+ * The classification of a record a call has just written, in any of its forms — a revoke's, a cancel's — for its
5741
+ * result to show through `publicApproval`, as a status, a wait and a list show theirs: what a sender wrote only inside
5742
+ * the untrusted-content envelope (design 2026-10-05 §D8). Classified as a look at it would be, against a fresh read of
5743
+ * the configuration, without the lock and writing nothing, as `approvalOf` is; a configuration that cannot be read
5744
+ * leaves nothing claimable, and never fails the call it reports on.
5745
+ */
5746
+ outcomeOf(stored: StoredApproval): Promise<ApprovalOutcome>;
5135
5747
  /** Issues a new challenge to show a human; only its hash is kept. */
5136
- issueChallenge(approvalId: string, kind?: ApprovalKind, platform?: NodeJS.Platform): Promise<string>;
5748
+ issueChallenge(approvalId: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
5749
+ usedSaid?: UsedSaid | undefined;
5750
+ }): Promise<string>;
5137
5751
  /**
5138
5752
  * A human approved through a confirm channel by typing the issued challenge. The draft must still be exactly what the
5139
5753
  * record was prepared for; otherwise the record is voided, because the human would be approving content the record
@@ -5142,18 +5756,35 @@ declare class ApprovalStore {
5142
5756
  * A change is approved the same way, with `kind: 'change'` and its digest standing in for the draft (see
5143
5757
  * `createChange`), so a person's typed code means one thing whichever kind of approval it is typed for.
5144
5758
  */
5145
- approve(approvalId: string, via: ApprovalChannel, live: LiveDraft, answer: string, kind?: ApprovalKind, platform?: NodeJS.Platform): Promise<ApprovalRecord>;
5759
+ approve(approvalId: string, via: ApprovalChannel, live: LiveDraft, answer: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
5760
+ usedSaid?: UsedSaid | undefined;
5761
+ }): Promise<ApprovalRecord>;
5146
5762
  /**
5147
5763
  * Claims the record for sending, once. Non-consuming refusals (not yet approved) leave the record untouched so the
5148
- * human can still approve it; integrity failures (other inbox or account, edited or changed draft, different
5149
- * recipients or subject) void it. Success creates the O_EXCL claim marker.
5764
+ * human can still approve it; integrity failures (another account, an edited or changed draft, different recipients
5765
+ * or subject) void it. Success creates the O_EXCL claim marker.
5766
+ *
5767
+ * `live.inboxId` is the owner the caller claims for: a record of another owner is the one `NOT_FOUND`, and left as it
5768
+ * is. The policy is the live gate's, from the configuration read under the record's lock (decision 4): a value the
5769
+ * caller read before the lock is exactly what a `never` change can land between.
5150
5770
  */
5151
5771
  claimForSend(approvalId: string, live: LiveDraft & {
5152
5772
  inboxId: string;
5153
5773
  inboxSub?: string | undefined;
5154
- policy: SendPolicy;
5155
5774
  expect: Expectation;
5156
- }, options?: ClaimOptions): Promise<ApprovalRecord>;
5775
+ }, options?: ClaimOptions): Promise<SendClaim>;
5776
+ /**
5777
+ * Renews the sending lease of a send this call claimed: writes `sendingHeartbeatAt` while the claim still owns a
5778
+ * `sending` record, and nothing otherwise. `renewed` while it is sending; `lost` once it reads `unknown` (or anything
5779
+ * else) — a renewal never moves a record back to `sending`. Refused, writing nothing, without the claim's token.
5780
+ */
5781
+ heartbeat(approvalId: string, claimToken: string): Promise<"renewed" | "lost">;
5782
+ /**
5783
+ * The check before each provider step (design 2026-10-05 §D1): `go`, with the lease renewed, while this claim still
5784
+ * owns a `sending` record; `stop` once it reads `unknown` — another caller has decided its outcome cannot be known,
5785
+ * and no further step may start. Refused, writing nothing, without the claim's token.
5786
+ */
5787
+ fence(approvalId: string, claimToken: string): Promise<"go" | "stop">;
5157
5788
  /**
5158
5789
  * A change approval, pending, bound to `changeDigest(input.change)`.
5159
5790
  *
@@ -5184,6 +5815,8 @@ declare class ApprovalStore {
5184
5815
  * other changes are approved. A claim holds the question to the stricter of this and the policy then.
5185
5816
  */
5186
5817
  createDownload(input: {
5818
+ /** The manifest channel the files come from. */
5819
+ channel: string;
5187
5820
  download: DownloadBinding;
5188
5821
  policy: ChangePolicy;
5189
5822
  }): Promise<ApprovalRecord & {
@@ -5196,7 +5829,7 @@ declare class ApprovalStore {
5196
5829
  *
5197
5830
  * Only a pending question can be answered, and only once: a second answer is refused, not taken over the first.
5198
5831
  */
5199
- answerDownload(approvalId: string, via: ApprovalChannel, answer: RecordedSaveAnswer, platform?: NodeJS.Platform): Promise<ApprovalRecord & {
5832
+ answerDownload(approvalId: string, via: ApprovalChannel, answer: RecordedSaveAnswer, _platform?: NodeJS.Platform): Promise<ApprovalRecord & {
5200
5833
  download: DownloadBinding;
5201
5834
  }>;
5202
5835
  /**
@@ -5209,17 +5842,17 @@ declare class ApprovalStore {
5209
5842
  * open, as it was: a second call that got an argument wrong is the agent's slip, not the person's, and voiding the
5210
5843
  * question for it would make the person answer again for nothing. It still expires, and is claimed once.
5211
5844
  *
5212
- * `options.policy` is the change policy of that account now, and the stricter of it and the one the question was
5213
- * asked under decides. Under `chat`, a pending question is claimed with the answer the caller carries: the person
5214
- * gave it in the conversation. Under `confirm`, only a question the person answered at a terminal or in a trusted
5215
- * form can be claimed; a pending one is refused, and left as it is, so they can still answer it. One used, voided or
5216
- * expired is refused as an approval in that state is.
5845
+ * Whether it can be claimed is the download `claimable` matrix (`downloadClaimable`, design 2026-10-05 §D2), decided
5846
+ * under the question's lock against the live change policy read there — never a policy the caller read before it.
5847
+ * While both the policy the question was asked under and the live one are `chat`, a pending question is claimed
5848
+ * with the answer the caller carries: the person gave it in the conversation, and it is never kept. Otherwise only a
5849
+ * question the person answered at a terminal or in a trusted form can be claimed; a pending one is refused, and left
5850
+ * as it is, so they can still answer it. One used, voided, expired or corrupt is refused for what it is.
5217
5851
  *
5218
5852
  * `options.signal` is the download's cancellation, asked under the lock as a send's is (`ClaimOptions.signal`): a
5219
5853
  * download cancelled while this waited saves nothing and leaves the question open, the answer still unused.
5220
5854
  */
5221
5855
  claimForDownload(approvalId: string, live: DownloadRequest, options?: {
5222
- policy?: ChangePolicy | undefined;
5223
5856
  pendingHint?: string | undefined;
5224
5857
  signal?: AbortSignal | undefined;
5225
5858
  platform?: NodeJS.Platform | undefined;
@@ -5227,72 +5860,76 @@ declare class ApprovalStore {
5227
5860
  }): Promise<ApprovalRecord & {
5228
5861
  download: DownloadBinding;
5229
5862
  }>;
5230
- /** Records the outcome of the one send attempt. */
5231
- complete(approvalId: string, outcome: {
5232
- sentMessageId: string;
5233
- } | {
5234
- error: string;
5235
- }): Promise<ApprovalRecord>;
5236
- /** Voids a pending or approved record (user revoked it, or policy tightened). Other records are left as they are. */
5237
- revoke(approvalId: string, reason: string): Promise<ApprovalRecord>;
5238
- list(filter?: {
5239
- inboxId?: string;
5240
- states?: ApprovalState[];
5241
- }): Promise<ApprovalRecord[]>;
5242
- }
5243
- //#endregion
5244
- //#region src/audit.d.ts
5245
- /**
5246
- * Every mailbox write leaves one line here: what was done, to which inbox, with which ids and outcome. Never message
5247
- * bodies, never secrets — recipient *domains* only, so the log can be shared when asking for help.
5248
- */
5249
- interface AuditRecord {
5250
- at: string;
5251
- /** Immutable inbox id; `alias` is the label at the time. */
5252
- inboxId: string;
5253
- alias?: string;
5254
- operation: string;
5255
- /** `started` marks the first half of a change recorded before it is made; a later record for the same ids ends it. */
5256
- outcome: "ok" | "refused" | "failed" | "started";
5257
- ids?: Record<string, string | string[] | CondensedIds>;
5258
- recipientDomains?: string[];
5259
5863
  /**
5260
- * Full canonical recipient addresses. Only a send writes these: for every other operation the domains are enough,
5261
- * and storing less is better — but when mail has actually left, "who did it go to" is the first question anyone
5262
- * asks afterwards, and a domain does not answer it.
5864
+ * Records the outcome of the one send attempt: `used` with the provider's id for it, when the provider accepted it,
5865
+ * or `failed`.
5866
+ *
5867
+ * `used` needs a non-empty id. A provider that accepted a send without one has not given anything to record it by,
5868
+ * so the record is left as it is — `sending`, and then `unknown` — rather than made `used` with an empty id that
5869
+ * every later reader would take for one.
5870
+ *
5871
+ * Only the claimant records it, by its claim token, from `sending` or from `unknown`: a provider's answer that
5872
+ * arrives after the lease ran out is still the truth. A late success clears the stale-lease reason; a late failure
5873
+ * replaces it with its own.
5263
5874
  */
5264
- recipients?: string[];
5265
- approvalId?: string;
5266
- reason?: string;
5267
- surface?: "cli" | "mcp";
5875
+ complete(approvalId: string, claimToken: string, outcome: SendOutcome): Promise<ApprovalRecord>;
5268
5876
  /**
5269
- * The policy that decided how this was approved. Written by change approvals, where it is the whole question
5270
- * afterwards: a loosening a person typed a code for and one an agent claimed after a yes in chat look the same in
5271
- * the configuration, and only this line tells them apart.
5877
+ * Voids a pending or approved record. Other records are left as they are, and returned as they read.
5878
+ *
5879
+ * `disposition` says whose decision this is, and has no default: every caller chooses (`RevokeDisposition`). A
5880
+ * version-1 record is retired in its own shape when a person or an owner's removal revokes it (`revokeLegacy`), and
5881
+ * refused with the version refusal, nothing written, when this release's integrity check would have voided it. A
5882
+ * corrupt or unreadable record is never revoked or rewritten: it is refused with the integrity refusal, which repeats
5883
+ * only its stub.
5884
+ */
5885
+ revoke(approvalId: string, reason: string, options: {
5886
+ disposition: RevokeDisposition;
5887
+ expect?: ApprovalExpectation | undefined;
5888
+ }): Promise<StoredApproval>;
5889
+ /**
5890
+ * Retires a record from an earlier release (version 1) in that release's own shape, so a process still running it
5891
+ * can no longer claim the record — and nothing more.
5892
+ *
5893
+ * Under the record's own lock, `<id>.json.lock`, the one 0.13.0's transitions take, so the two serialise. The file
5894
+ * is read again there, decoded, and its state derived by its own release's rules (`deriveLegacyV1State`). Only a
5895
+ * `pending` or `approved` record is rewritten, as itself plus `state: 'revoked'`, the reason and `updatedAt`: its
5896
+ * digest version stays 1, a send stays without a `kind`, and no field of this release's is added — no claim marker
5897
+ * either. Any other derived state is returned as it reads and written nothing: an expired record is never rewritten,
5898
+ * as `revoke` leaves every finished record as it is.
5899
+ *
5900
+ * It reads no configuration and needs no owner, so it works on a record whose account is already gone. Its only
5901
+ * callers are a person's revoke, the removal of the record's owner, and the legacy drain.
5272
5902
  */
5273
- policy?: SendPolicy;
5274
- }
5275
- /** Large id lists are condensed so every audit line stays small enough to be appended atomically. */
5276
- interface CondensedIds {
5277
- count: number;
5278
- sha256: string;
5279
- first: string[];
5280
- }
5281
- declare class AuditLog {
5282
- #private;
5283
- readonly directory: string;
5284
- constructor(stateDir: string, now?: () => Date);
5285
- append(record: Omit<AuditRecord, "at"> & {
5286
- at?: string;
5287
- }, options?: {
5288
- durable?: boolean;
5289
- }): Promise<AuditRecord>;
5290
- /** The most recent records, newest last, optionally filtered by inbox and a lower time bound. */
5291
- tail(options?: {
5292
- limit?: number;
5293
- inbox?: string;
5294
- since?: string;
5295
- }): Promise<AuditRecord[]>;
5903
+ revokeLegacy(approvalId: string, reason: string): Promise<LegacyApprovalRecord>;
5904
+ /**
5905
+ * Every approval file, each looked at under its own lock and classified there, as `inspect` looks at one — against one
5906
+ * configuration snapshot, loaded once before any file is read (design 2026-10-05 §D2, §D8). What a list shows: each
5907
+ * record's form and its outcome, nothing skipped. Like a look, it writes only what reading derives — an expiry at its
5908
+ * boundary, a stale send's `unknown` — never a revocation: an owner removed or a stale epoch is shown as revoked, and
5909
+ * written by the next action.
5910
+ *
5911
+ * `inboxId` keeps the records whose owner is that id (`ownerOf`), so one whose owner cannot be trusted never matches;
5912
+ * `states` keeps those whose classified state is listed — a stored state, `corrupt`, and for a download's answered
5913
+ * question its stored `approved` or `used`.
5914
+ */
5915
+ inspectAll(filter?: {
5916
+ inboxId?: string;
5917
+ states?: (ApprovalState | "corrupt")[];
5918
+ }): Promise<Array<{
5919
+ stored: StoredApproval;
5920
+ outcome: ApprovalOutcome;
5921
+ }>>;
5922
+ /**
5923
+ * Every approval file, each in one of its four forms — nothing skipped, nothing omitted — read against one
5924
+ * configuration snapshot, loaded once before any file is read: a loader that fails fails the whole list.
5925
+ *
5926
+ * `inboxId` keeps the records whose owner is that id (`ownerOf`), so a file whose owner cannot be trusted never
5927
+ * matches one; `states` keeps those whose state (`stateOf`, `corrupt` for one that cannot be used) is listed.
5928
+ */
5929
+ list(filter?: {
5930
+ inboxId?: string;
5931
+ states?: (ApprovalState | "corrupt")[];
5932
+ }): Promise<StoredApproval[]>;
5296
5933
  }
5297
5934
  //#endregion
5298
5935
  //#region src/ledger.d.ts
@@ -5456,9 +6093,23 @@ interface TaintExclusions {
5456
6093
  */
5457
6094
  ownHandles?: readonly TaintHandle[] | undefined;
5458
6095
  }
6096
+ /**
6097
+ * One stored aggregate, exactly as the store holds it (design 2026-10-05 §D4): the newest time anything matched it, the
6098
+ * strongest source ever seen — `header` is kept once seen — and every mailbox that recorded it. Three separate facts,
6099
+ * never one sighting: `touch` replaces the time, keeps a header and unions the mailboxes independently.
6100
+ */
6101
+ interface TaintAggregate {
6102
+ readonly at: string;
6103
+ readonly source: TaintSource;
6104
+ readonly inboxIds: readonly string[];
6105
+ }
5459
6106
  interface TaintCheck {
5460
6107
  address: boolean;
5461
6108
  domain: boolean;
6109
+ /** The exact address's aggregate, when it is in the window. */
6110
+ addressSeen?: TaintAggregate | undefined;
6111
+ /** Its domain's, when that counts — not a public mailbox provider — and is in the window. */
6112
+ domainSeen?: TaintAggregate | undefined;
5462
6113
  }
5463
6114
  declare class TaintStore {
5464
6115
  #private;
@@ -5469,7 +6120,10 @@ declare class TaintStore {
5469
6120
  * was not recorded.
5470
6121
  */
5471
6122
  record(observations: readonly TaintObservation[], exclusions: TaintExclusions, handleObservations?: readonly TaintHandleObservation[]): Promise<void>;
5472
- /** Whether an address, or its (non-public) domain, was seen in email content in the window — from any inbox. */
6123
+ /**
6124
+ * Whether an address, or its (non-public) domain, was seen in email content in the window — from any inbox — and the
6125
+ * aggregate each match is made of, for a caller to explain it. Nothing is written.
6126
+ */
5473
6127
  check(address: string): Promise<TaintCheck>;
5474
6128
  /**
5475
6129
  * Whether a handle was seen in message content in the window.