@agentcomms/resend 0.21.0 → 0.23.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.
- package/dist/cli.mjs +2 -2
- package/dist/index.d.mts +1689 -1404
- package/dist/index.mjs +2 -2
- package/dist/{send-tAIE9B1y.mjs → send-Dnq7bTQn.mjs} +2654 -458
- package/dist/{server-CQ9RE8L7.mjs → server-CCzhekl9.mjs} +1 -1
- package/dist/{stdio-entry-DPJKJhGj.mjs → stdio-entry-BWw-XnwO.mjs} +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -4322,26 +4322,7 @@ declare namespace external_d_exports {
|
|
|
4322
4322
|
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 };
|
|
4323
4323
|
}
|
|
4324
4324
|
//#endregion
|
|
4325
|
-
//#region ../core/dist/launcher-
|
|
4326
|
-
//#endregion
|
|
4327
|
-
//#region src/config-hooks.d.ts
|
|
4328
|
-
/**
|
|
4329
|
-
* Test-only pauses inside `ConfigStore.convertToVersion3`, for the tests that prove a write racing the conversion is
|
|
4330
|
-
* neither lost nor let in between its scan and its write.
|
|
4331
|
-
*
|
|
4332
|
-
* Deliberately not exported from the package root, as `enableNamesMigrationForTests` is not (`release-gate.ts`): a
|
|
4333
|
-
* store is given them through its constructor under this symbol, which only a test importing this file by path can
|
|
4334
|
-
* name. No consumer of the package can reach it.
|
|
4335
|
-
*/
|
|
4336
|
-
declare const CONVERSION_HOOKS: unique symbol;
|
|
4337
|
-
interface ConversionHooks {
|
|
4338
|
-
/** After the read before the lock, before the config lock is taken. */
|
|
4339
|
-
beforeLock?: (() => Promise<void> | void) | undefined;
|
|
4340
|
-
/** Under the lock, after the approvals were scanned and before the version-3 write is built. */
|
|
4341
|
-
afterScan?: (() => Promise<void> | void) | undefined;
|
|
4342
|
-
/** Under the lock, immediately before the version-3 write. */
|
|
4343
|
-
beforeWrite?: (() => Promise<void> | void) | undefined;
|
|
4344
|
-
}
|
|
4325
|
+
//#region ../core/dist/launcher-CD40rL4F.d.mts
|
|
4345
4326
|
interface RegisteredServer {
|
|
4346
4327
|
client: string;
|
|
4347
4328
|
path: string;
|
|
@@ -4415,19 +4396,67 @@ interface ResolvedPaths {
|
|
|
4415
4396
|
*/
|
|
4416
4397
|
downloadsDir: string;
|
|
4417
4398
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4399
|
+
//#endregion
|
|
4400
|
+
//#region src/approval-binding.d.ts
|
|
4401
|
+
/**
|
|
4402
|
+
* What a version-2 approval binds, defined once (design 2026-10-05 §D1, "Digest integrity").
|
|
4403
|
+
*
|
|
4404
|
+
* A record stores two digests. `contentDigest` is the outward content or change, computed as before: the message, the
|
|
4405
|
+
* change, the download. `bindingDigest` is this module's: the content digest together with the record's route, its
|
|
4406
|
+
* lifetimes and its own identity — who it belongs to, which draft, which channel, which send epoch. Every one of those
|
|
4407
|
+
* is a field a claim or a report acts on, so a record whose binding no longer recomputes from its own fields has been
|
|
4408
|
+
* altered since it was made, and nothing it says can be trusted.
|
|
4409
|
+
*
|
|
4410
|
+
* There is no second copy of anything: the binding is recomputed from the stored fields alone, never stored apart
|
|
4411
|
+
* from them as a key.
|
|
4412
|
+
*/
|
|
4413
|
+
/**
|
|
4414
|
+
* How an approval is decided, fixed when it is made: `chat` — a yes in the conversation is enough — or `confirm` — a
|
|
4415
|
+
* person outside the chat decides. A send's route is the stricter of its live send policy and any escalation; a
|
|
4416
|
+
* change's is the live change policy. It never changes when the configuration does.
|
|
4417
|
+
*
|
|
4418
|
+
* `standing` — the send is one use of a standing approval the person gave in advance, at a terminal with its code
|
|
4419
|
+
* (design 2026-10-19-standing-approvals, S9): a child, approved when it is made, for one claim within minutes. A
|
|
4420
|
+
* standing record itself is on `confirm`, whatever the change policy.
|
|
4421
|
+
*/
|
|
4422
|
+
type ApprovalRoute = "chat" | "confirm" | "standing";
|
|
4423
|
+
/** The four activation documents that may receive standing disclosure authority. */
|
|
4424
|
+
type DisclosureActivationKind = "rule" | "judge-budget" | "judge-kind" | "enable-all";
|
|
4425
|
+
/** An immutable version named by a standing disclosure authorisation. */
|
|
4426
|
+
type DisclosureVersionKind = "rule" | "target" | "subscriber" | "judge" | "judge-budget" | "judge-kind";
|
|
4427
|
+
interface DisclosureVersion {
|
|
4428
|
+
readonly kind: DisclosureVersionKind;
|
|
4429
|
+
readonly id: string;
|
|
4430
|
+
readonly version: number;
|
|
4423
4431
|
}
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4432
|
+
/**
|
|
4433
|
+
* The content-free, canonical authority for a future disclosure (local-event design D2).
|
|
4434
|
+
*
|
|
4435
|
+
* The event daemon derives this from one canonical activation document. Core does not construct
|
|
4436
|
+
* that document; it verifies the closed shape that makes an independently supplied version list
|
|
4437
|
+
* impossible to smuggle into a disclosure record.
|
|
4438
|
+
*/
|
|
4439
|
+
interface DisclosureBinding {
|
|
4440
|
+
readonly digest: string;
|
|
4441
|
+
readonly activationIntentId: string;
|
|
4442
|
+
readonly activationKind: DisclosureActivationKind;
|
|
4443
|
+
readonly versions: readonly DisclosureVersion[];
|
|
4444
|
+
}
|
|
4445
|
+
/**
|
|
4446
|
+
* What a record's `inboxId` names. `owner` — a mailbox or account that existed when it was made (every send and
|
|
4447
|
+
* download, and a change to one). `prospective` — a change that creates its owner, which has no id yet. `global` — a
|
|
4448
|
+
* change to no owner at all, stored with `inboxId: ''`.
|
|
4449
|
+
*/
|
|
4450
|
+
type OwnerScope = "owner" | "prospective" | "global";
|
|
4451
|
+
/**
|
|
4452
|
+
* One use of a standing approval, as its child is bound to it (design 2026-10-19-standing-approvals, S9): the channel
|
|
4453
|
+
* its act goes to, the thread it is in, and the digest of the content it sends — what a claim checks its standing
|
|
4454
|
+
* approval still covers.
|
|
4455
|
+
*/
|
|
4456
|
+
interface StandingUseBinding {
|
|
4457
|
+
channel: string;
|
|
4458
|
+
thread: string;
|
|
4459
|
+
content: string;
|
|
4431
4460
|
}
|
|
4432
4461
|
/**
|
|
4433
4462
|
* The file operations a lock is made of, one method each, so a test can count and interrupt every one of them: the
|
|
@@ -4449,1039 +4478,804 @@ interface LockIo {
|
|
|
4449
4478
|
remove(path: string): Promise<void>;
|
|
4450
4479
|
}
|
|
4451
4480
|
//#endregion
|
|
4452
|
-
//#region src/
|
|
4453
|
-
/**
|
|
4454
|
-
* The command a person is told to run for one of this suite's own CLIs (design 2026-10-04, D1 and D5; CUE-403).
|
|
4455
|
-
*
|
|
4456
|
-
* A bare `agent-gmail approve …` is not runnable for most of the people it is printed for: a package run by npx, by a
|
|
4457
|
-
* managed runtime or from a checkout puts nothing on their PATH. So every such command names this process's Node and
|
|
4458
|
-
* a checked file of the product it runs, with the suite directories it uses pinned as options — and when no file can
|
|
4459
|
-
* be checked, there is no command at all, only a sentence saying what is missing. The caller says which package it is
|
|
4460
|
-
* and what to run; it never supplies the program or quotes the line.
|
|
4461
|
-
*
|
|
4462
|
-
* Three directions. A product's own CLI is found from the product itself, walking from one of its own modules to its
|
|
4463
|
-
* manifest: no registration is ever asked. A channel's handoff to core is found the way the channel itself finds core,
|
|
4464
|
-
* through its installed dependency, and only the same release. Another product's command is found among the servers
|
|
4465
|
-
* registered with the person's MCP clients, which the caller scanned and passes in: only a file-backed registration of
|
|
4466
|
-
* exactly the printing package's version, checked on disk. Nothing a registration names is run, or asked anything —
|
|
4467
|
-
* not even its interpreter for a version: no installer wrote down where a scanned entry came from, so none is trusted
|
|
4468
|
-
* to run (D1). This module never scans, spawns or probes.
|
|
4469
|
-
*
|
|
4470
|
-
* What is checked is checked when the command is printed, not for ever (D6): the file it names is real, readable and
|
|
4471
|
-
* inside its package then, and a later change to that installation is a later fact.
|
|
4472
|
-
*/
|
|
4473
|
-
/** The gate a constructor call has to pass: only this module holds it. */
|
|
4474
|
-
declare const GATE$1: unique symbol;
|
|
4481
|
+
//#region src/approval-io.d.ts
|
|
4475
4482
|
/**
|
|
4476
|
-
*
|
|
4477
|
-
*
|
|
4478
|
-
*
|
|
4483
|
+
* The file operations approval maintenance and the unsent report are made of (design 2026-10-05 §D9), one method
|
|
4484
|
+
* each, so that a test can count every one of them — every directory read, metadata stat, record open, lock try,
|
|
4485
|
+
* renewal, ownership check and unlink — and none is hidden behind a helper. The store passes the real ones unless it
|
|
4486
|
+
* was opened with others.
|
|
4479
4487
|
*
|
|
4480
|
-
*
|
|
4481
|
-
* a spread copy of one with other words in it, as it would be with a symbol-keyed brand. Only the type is exported;
|
|
4482
|
-
* the class and its gate are not.
|
|
4488
|
+
* Only these two passes go through it: every other read and write of the store is as it was.
|
|
4483
4489
|
*/
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
/**
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
* leaves the process, so a field can hold the command itself and the wire stays the same.
|
|
4502
|
-
*/
|
|
4503
|
-
toJSON(): string;
|
|
4504
|
-
/** Never interpolated: a sentence gives a command through `handoffSentence`, a value through `handoffText`. */
|
|
4505
|
-
[Symbol.toPrimitive](): never;
|
|
4506
|
-
}
|
|
4507
|
-
/** The package asking: a module of its own (`import.meta.url`), and the package that module is expected to be in. */
|
|
4508
|
-
interface CliCommandCaller {
|
|
4509
|
-
readonly url: string;
|
|
4510
|
-
readonly packageName: string;
|
|
4490
|
+
interface ApprovalIo {
|
|
4491
|
+
/** One enumeration of a directory: its entries' names. */
|
|
4492
|
+
readdir(directory: string): Promise<string[]>;
|
|
4493
|
+
/** One metadata stat: the modification time, in milliseconds. */
|
|
4494
|
+
stat(path: string): Promise<{
|
|
4495
|
+
readonly mtimeMs: number;
|
|
4496
|
+
}>;
|
|
4497
|
+
/** One open of a file's content, as text. */
|
|
4498
|
+
readText(path: string): Promise<string>;
|
|
4499
|
+
/** Replaces a file atomically — a temporary file, flushed, renamed — and, when `durable`, syncs its directory too. */
|
|
4500
|
+
writeAtomic(path: string, text: string, options?: {
|
|
4501
|
+
durable?: boolean;
|
|
4502
|
+
}): Promise<void>;
|
|
4503
|
+
/** One unlink of an approval artifact: a record or its claim marker. A missing one rejects with `ENOENT`. */
|
|
4504
|
+
unlink(path: string): Promise<void>;
|
|
4505
|
+
/** The lock files' own operations (`tryFileLock`). */
|
|
4506
|
+
readonly lock: LockIo;
|
|
4511
4507
|
}
|
|
4512
|
-
/**
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4508
|
+
/** A version-1 record, as 0.13.0 wrote it: a send has no `kind`, and one `digest` stands for the content. */
|
|
4509
|
+
interface LegacyApprovalRecord {
|
|
4510
|
+
approvalId: string;
|
|
4511
|
+
/** Absent on a send. */
|
|
4512
|
+
kind?: ApprovalKind | undefined;
|
|
4513
|
+
digestVersion: 1;
|
|
4514
|
+
inboxId: string;
|
|
4515
|
+
inboxSub?: string | undefined;
|
|
4516
|
+
draftId: string;
|
|
4517
|
+
draftMessageId: string;
|
|
4518
|
+
digest: string;
|
|
4519
|
+
approvedDigest?: string | undefined;
|
|
4520
|
+
approvedVia?: ApprovalChannel | undefined;
|
|
4521
|
+
policy: SendPolicy;
|
|
4522
|
+
requiredPolicy: SendPolicy;
|
|
4523
|
+
riskFlags: string[];
|
|
4524
|
+
expect: Expectation;
|
|
4525
|
+
challengeHash?: string | undefined;
|
|
4526
|
+
challengeAttempts: number;
|
|
4527
|
+
state: ApprovalState;
|
|
4528
|
+
createdAt: string;
|
|
4529
|
+
expiresAt: string;
|
|
4530
|
+
updatedAt: string;
|
|
4531
|
+
sentMessageId?: string | undefined;
|
|
4532
|
+
reason?: string | undefined;
|
|
4533
|
+
change?: ChangeBinding | undefined;
|
|
4534
|
+
download?: DownloadBinding | undefined;
|
|
4521
4535
|
}
|
|
4522
4536
|
/**
|
|
4523
|
-
*
|
|
4524
|
-
*
|
|
4537
|
+
* A version-1 record as every surface shows it: `{ approvalId, kind, state, legacy: true, createdAt, expiresAt }` and
|
|
4538
|
+
* the summary a version-2 record shows — its owner, what it was for, and which channel made it when that can be told.
|
|
4539
|
+
* Never claimable, and never given a version-2 timestamp it does not hold.
|
|
4525
4540
|
*/
|
|
4526
|
-
interface
|
|
4527
|
-
readonly
|
|
4528
|
-
readonly
|
|
4529
|
-
/**
|
|
4530
|
-
readonly
|
|
4531
|
-
readonly
|
|
4532
|
-
|
|
4533
|
-
readonly
|
|
4534
|
-
/**
|
|
4535
|
-
readonly
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4541
|
+
interface LegacyView {
|
|
4542
|
+
readonly approvalId: string;
|
|
4543
|
+
readonly kind: ApprovalKind;
|
|
4544
|
+
/** Derived as 0.13.0 derived it (`deriveLegacyV1State`). */
|
|
4545
|
+
readonly state: ApprovalState;
|
|
4546
|
+
readonly legacy: true;
|
|
4547
|
+
readonly createdAt: string;
|
|
4548
|
+
readonly expiresAt: string;
|
|
4549
|
+
/** The stored owner, trusted as stored: a version-1 record has no binding to check it against. */
|
|
4550
|
+
readonly inboxId: string;
|
|
4551
|
+
readonly ownerScope: "owner" | "global";
|
|
4552
|
+
/**
|
|
4553
|
+
* The channel that made it, when only one could have: an `ibx_` owner is a Gmail mailbox, an `acc_` owner the
|
|
4554
|
+
* stored platform of an account that still exists. Null when nothing says — it then takes part in no channel's
|
|
4555
|
+
* grouping and gets only generic wording.
|
|
4556
|
+
*/
|
|
4557
|
+
readonly channel: string | null;
|
|
4558
|
+
readonly expect: Expectation;
|
|
4559
|
+
readonly reason?: string | undefined;
|
|
4560
|
+
readonly sentMessageId?: string | undefined;
|
|
4542
4561
|
}
|
|
4543
4562
|
//#endregion
|
|
4544
|
-
//#region src/
|
|
4563
|
+
//#region src/audit.d.ts
|
|
4545
4564
|
/**
|
|
4546
|
-
*
|
|
4547
|
-
*
|
|
4548
|
-
*
|
|
4549
|
-
* Kept apart from the locator on purpose: the configuration store, the secret stores and the attachment jail print
|
|
4550
|
-
* handoffs too, and the locator's own imports reach the configuration store, so importing it from them would be a
|
|
4551
|
-
* cycle. Nothing here locates anything or imports a module that does: a located command is made by `handoffs.ts` and
|
|
4552
|
-
* handed in, and what is here only renders one. With none handed in, there is nothing to render: a package that did not
|
|
4553
|
-
* give core its caller and is asked for a command has a programming error, not a bare name to fall back on.
|
|
4565
|
+
* Every mailbox write leaves one line here: what was done, to which inbox, with which ids and outcome. Never message
|
|
4566
|
+
* bodies, never secrets — recipient *domains* only, so the log can be shared when asking for help.
|
|
4554
4567
|
*/
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
* `cliHandoffs`): `openCore({ caller })` puts them on `core.handoffs`. Each method takes the CLI's own words, never the
|
|
4566
|
-
* program, and returns a `Handoff`.
|
|
4567
|
-
*/
|
|
4568
|
-
interface CliHandoffs {
|
|
4569
|
-
/** The printing package: one of its own modules and its name. */
|
|
4570
|
-
readonly caller: CliCommandCaller;
|
|
4571
|
-
/** The shell the commands are quoted for. */
|
|
4572
|
-
readonly platform: NodeJS.Platform;
|
|
4573
|
-
/** The printing package's own CLI, with these words after the program. */
|
|
4574
|
-
own(words: readonly string[], use?: HandoffUse): Handoff;
|
|
4575
|
-
/** The core CLI: the printing package's own when that is core, the core it has installed otherwise. */
|
|
4576
|
-
core(words: readonly string[], use?: HandoffUse): Handoff;
|
|
4577
|
-
/** The CLI of `channel` — `core`, `gmail`, `slack` …: own, core, or another product found among its registrations. */
|
|
4578
|
-
of(channel: string, words: readonly string[], use?: HandoffUse): Handoff;
|
|
4579
|
-
/** The same commands, quoted for another shell. */
|
|
4580
|
-
on(platform: NodeJS.Platform): CliHandoffs;
|
|
4581
|
-
/**
|
|
4582
|
-
* The same, able to find another product: with the servers registered with this machine's MCP clients read now, from
|
|
4583
|
-
* the environment these were made with. Read once per call; nothing a registration names is run.
|
|
4584
|
-
*/
|
|
4585
|
-
registered(): Promise<CliHandoffs>;
|
|
4586
|
-
/** The same, finding another product among these registrations: a scan the caller has already read. */
|
|
4587
|
-
withRegistrations(registrations: readonly RegisteredServer[]): CliHandoffs;
|
|
4588
|
-
}
|
|
4589
|
-
type SendPolicy = "chat" | "confirm" | "never";
|
|
4590
|
-
/**
|
|
4591
|
-
* How a loosening of the configuration is approved: `chat` — the person says yes in the conversation and the agent
|
|
4592
|
-
* claims the approval — or `confirm` — the person types a code at a terminal, as for a send under `confirm`.
|
|
4593
|
-
*
|
|
4594
|
-
* There is no `never`. A setting the software refused ever to loosen would leave editing the file by hand as the only
|
|
4595
|
-
* way to change it, and that passes no gate at all.
|
|
4596
|
-
*/
|
|
4597
|
-
type ChangePolicy = "chat" | "confirm";
|
|
4598
|
-
/** 0.18's switch for the daily update check: what `defaults.updates` reads from until a level is set. */
|
|
4599
|
-
type UpdateCheckSetting = "on" | "off";
|
|
4600
|
-
type StoreKind = "keychain" | "file";
|
|
4601
|
-
interface ClientConfig {
|
|
4602
|
-
provider: string;
|
|
4603
|
-
clientId: string;
|
|
4604
|
-
projectId?: string | undefined;
|
|
4605
|
-
secretRef: string;
|
|
4606
|
-
addedAt: string;
|
|
4607
|
-
/**
|
|
4608
|
-
* The organisation profile that made this row and owns it (design 2026-10-02 §D4) — absent on a row a person
|
|
4609
|
-
* registered, or one a profile adopted. A row carrying it is the profile's to change: `org update` and `org remove`
|
|
4610
|
-
* change it, and `client add --replace` and `client remove` refuse it while its organisation has a record.
|
|
4611
|
-
*
|
|
4612
|
-
* Additive: client rows are loose, so a release that does not know the key keeps it through its writes.
|
|
4613
|
-
*/
|
|
4614
|
-
organisation?: string | undefined;
|
|
4615
|
-
}
|
|
4616
|
-
interface InboxConfig {
|
|
4617
|
-
/** Immutable id (`ibx_` + 16 base32 characters). Secrets and all state are keyed by it; the alias is a label. */
|
|
4618
|
-
id: string;
|
|
4619
|
-
provider: string;
|
|
4620
|
-
email: string;
|
|
4621
|
-
/** Stable account id (OIDC `sub`). Absent for inboxes imported from tools that never asked for it. */
|
|
4622
|
-
sub?: string | undefined;
|
|
4623
|
-
identity: "oidc" | "legacy";
|
|
4624
|
-
client: string;
|
|
4625
|
-
tier: string;
|
|
4626
|
-
contacts: boolean;
|
|
4627
|
-
grantedScopes: string[];
|
|
4628
|
-
secretRef: string;
|
|
4629
|
-
sendPolicy?: SendPolicy | undefined;
|
|
4630
|
-
/** Overrides `defaults.changePolicy` for changes to this inbox. */
|
|
4631
|
-
changePolicy?: ChangePolicy | undefined;
|
|
4632
|
-
internalDomains: string[];
|
|
4633
|
-
createdAt: string;
|
|
4634
|
-
}
|
|
4635
|
-
interface Defaults {
|
|
4636
|
-
sendPolicy: SendPolicy;
|
|
4637
|
-
/**
|
|
4638
|
-
* How a loosening is approved wherever an inbox or account does not say otherwise. Absent reads as `chat`.
|
|
4639
|
-
*
|
|
4640
|
-
* Absent rather than filled in by the schema, unlike `sendPolicy`. A config written before this setting existed
|
|
4641
|
-
* reads as `chat` from then on, and the release note says so; a default the schema supplied would be written into
|
|
4642
|
-
* the file by the next unrelated change, where it would look like a choice somebody made.
|
|
4643
|
-
*/
|
|
4644
|
-
changePolicy?: ChangePolicy | undefined;
|
|
4645
|
-
riskEscalation: boolean;
|
|
4646
|
-
sendCaps: {
|
|
4647
|
-
perHour: number;
|
|
4648
|
-
perDay: number;
|
|
4649
|
-
};
|
|
4650
|
-
attachRoots: string[];
|
|
4651
|
-
/** Extra deny entries on top of the built-in list. */
|
|
4652
|
-
attachDeny: string[];
|
|
4653
|
-
downloadsDir?: string | undefined;
|
|
4654
|
-
timezone: string;
|
|
4655
|
-
confirm: {
|
|
4656
|
-
/**
|
|
4657
|
-
* Fail-closed allowlist of MCP `clientInfo.name` values whose form elicitation is trusted to reach a human.
|
|
4658
|
-
* Empty by default; only the CLI adds entries, after a probe.
|
|
4659
|
-
*/
|
|
4660
|
-
elicitationClients: string[];
|
|
4661
|
-
};
|
|
4662
|
-
/**
|
|
4663
|
-
* The daily update check (design 2026-09-28): once a day this machine asks npm whether a newer release is out, and
|
|
4664
|
-
* every server and command stops until it is updated or put off until tomorrow. Absent reads as `on`.
|
|
4665
|
-
*
|
|
4666
|
-
* Absent rather than filled in, as `changePolicy` is: a default the schema supplied would be written into the file
|
|
4667
|
-
* by the next unrelated change, where it would look like a choice somebody made. Turning it off is a loosening —
|
|
4668
|
-
* it is what keeps a machine from quietly falling behind a release — so it is approved like one; on applies at once.
|
|
4669
|
-
*/
|
|
4670
|
-
updateCheck?: UpdateCheckSetting | undefined;
|
|
4671
|
-
/**
|
|
4672
|
-
* How this machine updates (design 2026-10-16, U2): `auto`, `every-14-days` or `never`. Absent reads from
|
|
4673
|
-
* `updateCheck` — `on` as `auto`, `off` as `never` — and both absent as `auto`. A word this release does not know
|
|
4674
|
-
* reads as `auto`, and the file stays readable: a later release may add a level.
|
|
4675
|
-
*
|
|
4676
|
-
* Moving to a level that updates less is a loosening, approved like one; moving back applies at once. Every write of
|
|
4677
|
-
* a level writes `updateCheck` beside it, `off` for `never` and `on` otherwise, so a release before 0.20 sharing the
|
|
4678
|
-
* file checks, or does not, as the person chose.
|
|
4679
|
-
*/
|
|
4680
|
-
updates?: string | undefined;
|
|
4681
|
-
}
|
|
4682
|
-
/**
|
|
4683
|
-
* A connected account on a platform that is not mail: a Slack workspace, and every channel after it (design
|
|
4684
|
-
* 2026-09-26) — one generic record, whatever the platform.
|
|
4685
|
-
*
|
|
4686
|
-
* `id`, `platform`, `workspace` (the container: a Slack team, a Resend team, a WhatsApp store), `userId` (who it acts
|
|
4687
|
-
* as), `tier`, `mode`, `grantedScopes`, `secretRef`, the two policies and `createdAt` are every channel's. A channel
|
|
4688
|
-
* may keep keys of its own beside them — Slack's `appId`, `redirectPort` — and they are kept through every write. It
|
|
4689
|
-
* may not add a safety setting: only `sendPolicy`, `changePolicy` and `mode` are judged by `classifyChange`, so a key
|
|
4690
|
-
* of a channel's own that loosened something would loosen it unasked.
|
|
4691
|
-
*
|
|
4692
|
-
* `secretRef` stays required, although the 2026-09-26 contract made it optional for a channel with no credential:
|
|
4693
|
-
* every earlier release requires it, and they share this file — an account without one would make the whole
|
|
4694
|
-
* configuration unreadable to a server started last week. Such a channel records a reference that names no secret,
|
|
4695
|
-
* `<platform>:none:<id>`, until a config version can say otherwise.
|
|
4696
|
-
*
|
|
4697
|
-
* It sits beside `inboxes` rather than replacing it. The spec asked for one `accounts` map holding everything, and
|
|
4698
|
-
* that rename is the one change this file cannot take: `version: 1` is additive precisely because an MCP server
|
|
4699
|
-
* started last week and a CLI run today share the file, and a release that moved every mailbox out of `inboxes`
|
|
4700
|
-
* would read, to the older of the two, as a config with no mailboxes in it. What the rename was for — one list of
|
|
4701
|
-
* everything connected, whatever the platform — is a question about the shape of the answer, not the shape of the
|
|
4702
|
-
* file, so `connectedAccounts()` provides it and the file grows one key.
|
|
4703
|
-
*/
|
|
4704
|
-
interface AccountConfig {
|
|
4705
|
-
id: string;
|
|
4706
|
-
/** `slack`. */
|
|
4707
|
-
platform: string;
|
|
4708
|
-
/** The workspace or team id. Every other id this account sees is only meaningful inside it. */
|
|
4709
|
-
workspace: string;
|
|
4710
|
-
/** "Acme Corp" — shown to people, never matched on: a workspace can be renamed and stays the same workspace. */
|
|
4711
|
-
workspaceName?: string | undefined;
|
|
4712
|
-
/** This account's own user id in that workspace, so its own messages can be told from everyone else's. */
|
|
4713
|
-
userId: string;
|
|
4714
|
-
tier: string;
|
|
4715
|
-
grantedScopes: string[];
|
|
4716
|
-
/** Where its credential is in the secret store, `<platform>:…` — for a channel that stores none, `<platform>:none:<id>`. */
|
|
4717
|
-
secretRef: string;
|
|
4718
|
-
sendPolicy?: SendPolicy | undefined;
|
|
4719
|
-
/** Overrides `defaults.changePolicy` for changes to this account. */
|
|
4720
|
-
changePolicy?: ChangePolicy | undefined;
|
|
4721
|
-
createdAt: string;
|
|
4722
|
-
/**
|
|
4723
|
-
* The OAuth client this account's token was issued by, and the app it belongs to.
|
|
4724
|
-
*
|
|
4725
|
-
* Recorded because the Gmail release found the opposite: a reauth used the first OAuth client in the config
|
|
4726
|
-
* rather than the inbox's own, and then did not record which one it had used. Slack makes that worse — D8
|
|
4727
|
-
* means **one app per workspace**, so "the first app" is wrong more often than it is right — and a reauth
|
|
4728
|
-
* that silently moves an account onto a different app changes what it can do without saying so.
|
|
4729
|
-
*
|
|
4730
|
-
* Optional because the key is additive: a config written before these existed parses unchanged.
|
|
4731
|
-
*/
|
|
4732
|
-
oauthClientId?: string | undefined;
|
|
4733
|
-
appId?: string | undefined;
|
|
4734
|
-
/**
|
|
4735
|
-
* `read` or `send`, as installed.
|
|
4736
|
-
*
|
|
4737
|
-
* Kept beside `grantedScopes` rather than derived from them, because the two answer different questions: the
|
|
4738
|
-
* scopes are what Slack granted, and this is what the person asked for. A disagreement between them is drift
|
|
4739
|
-
* worth reporting, and a value derived from the scopes could never disagree.
|
|
4740
|
-
*/
|
|
4741
|
-
mode?: string | undefined;
|
|
4742
|
-
/**
|
|
4743
|
-
* The loopback port the last sign-in used, which is the one in the app's redirect URL.
|
|
4744
|
-
*
|
|
4745
|
-
* Slack matches redirect URLs exactly, so every later sign-in and every set of steps that edits the app has to
|
|
4746
|
-
* name this number. Without it the steps said `<port>`, or — from the MCP server — guessed 51234, and a person who
|
|
4747
|
-
* had chosen another port followed them to a sign-in that failed on the way back.
|
|
4748
|
-
*/
|
|
4749
|
-
redirectPort?: number | undefined;
|
|
4750
|
-
/** The organisation profile whose Slack app created this account. Absent for a person's own app. */
|
|
4751
|
-
organisation?: string | undefined;
|
|
4752
|
-
/** Which app in that profile issued the account's current credential. */
|
|
4753
|
-
profileApp?: "read" | "send" | undefined;
|
|
4754
|
-
}
|
|
4755
|
-
type PendingRevocationStatus = "pending" | "revoked" | "expired";
|
|
4756
|
-
/** One bearer token in a superseded rotating bundle, tracked independently from its pair. */
|
|
4757
|
-
interface PendingRevocationTokenState {
|
|
4758
|
-
status: PendingRevocationStatus;
|
|
4759
|
-
/** The fixed absolute time after which the old token is known to be unusable. */
|
|
4760
|
-
deadline: string;
|
|
4761
|
-
}
|
|
4762
|
-
/**
|
|
4763
|
-
* A superseded credential kept until each token in it is conclusively revoked or expired.
|
|
4764
|
-
*
|
|
4765
|
-
* `store` belongs to the entry rather than being inferred from `secrets.store`: an older release can move the root
|
|
4766
|
-
* store without knowing to move this bundle, and cleanup must still find the only copy that exists.
|
|
4767
|
-
*/
|
|
4768
|
-
interface PendingRevocation {
|
|
4769
|
-
ref: string;
|
|
4770
|
-
store: StoreKind;
|
|
4771
|
-
platform: string;
|
|
4772
|
-
workspace: string;
|
|
4773
|
-
createdAt: string;
|
|
4774
|
-
tokens: {
|
|
4775
|
-
access: PendingRevocationTokenState;
|
|
4776
|
-
refresh?: PendingRevocationTokenState | undefined;
|
|
4777
|
-
};
|
|
4778
|
-
}
|
|
4779
|
-
interface ConfigBody {
|
|
4780
|
-
/**
|
|
4781
|
-
* The one secret backend for this config directory: client secrets, refresh tokens and the approval key. Absent
|
|
4782
|
-
* until the first command that stores a secret chooses it.
|
|
4783
|
-
*/
|
|
4784
|
-
secrets?: {
|
|
4785
|
-
store: StoreKind;
|
|
4786
|
-
} | undefined;
|
|
4787
|
-
clients: Record<string, ClientConfig>;
|
|
4788
|
-
inboxes: Record<string, InboxConfig>;
|
|
4789
|
-
/** Non-mail accounts. Absent in every config written before this key existed, hence the default. */
|
|
4790
|
-
accounts: Record<string, AccountConfig>;
|
|
4791
|
-
/** Superseded rotating credentials which still need conclusive per-token cleanup. */
|
|
4792
|
-
pendingRevocations?: PendingRevocation[] | undefined;
|
|
4793
|
-
defaults: Defaults;
|
|
4794
|
-
/**
|
|
4795
|
-
* How what an agent sends is labelled (design 2026-10-15-agent-label, L5): the word, the name, the rules that say
|
|
4796
|
-
* who a message appears to come from, and each Slack account's agent emoji.
|
|
4797
|
-
*
|
|
4798
|
-
* `unknown` here, on purpose: it is read only through `agentLabelOf`, which reads a value it does not understand
|
|
4799
|
-
* as the stricter one — an unknown word as `agent`, a rule list it cannot read as no rules — so a hand edit or a
|
|
4800
|
-
* later release's key can only tighten. Kept through every write as the schemas keep every key they do not name, and
|
|
4801
|
-
* judged by `classifyChange`.
|
|
4802
|
-
*/
|
|
4803
|
-
agentLabel?: unknown;
|
|
4804
|
-
}
|
|
4805
|
-
interface ConfigV1 extends ConfigBody {
|
|
4806
|
-
version: 1;
|
|
4807
|
-
}
|
|
4808
|
-
/** A name an account used to have, and the account that had it. */
|
|
4809
|
-
interface FormerName {
|
|
4810
|
-
/** What it was renamed to — at the time. The account's current name is found by `id`, so a later rename is followed. */
|
|
4811
|
-
name: string;
|
|
4812
|
-
id: string;
|
|
4813
|
-
}
|
|
4814
|
-
/**
|
|
4815
|
-
* Names that were replaced, per kind, and never reusable.
|
|
4816
|
-
*
|
|
4817
|
-
* Per kind because version 1 lets a mailbox and a workspace share a word: `work` the mailbox and `work` the workspace
|
|
4818
|
-
* become `work/gmail` and `work/slack`, and one flat record could not say which old `work` is which.
|
|
4819
|
-
*/
|
|
4820
|
-
interface FormerNames {
|
|
4821
|
-
inboxes: Record<string, FormerName>;
|
|
4822
|
-
accounts: Record<string, FormerName>;
|
|
4823
|
-
}
|
|
4824
|
-
/** Who an organisation's Google client is for, as its administrator states it (design 2026-10-02 §D2). */
|
|
4825
|
-
type OrganisationServes = "any" | {
|
|
4826
|
-
domains: string[];
|
|
4827
|
-
};
|
|
4828
|
-
/**
|
|
4829
|
-
* One Google client an organisation profile brought here: a *generation* (design 2026-10-02 §D4).
|
|
4830
|
-
*
|
|
4831
|
-
* `owned` — the profile made the client row `name` (`rgc-1`), which carries `organisation`, and its secret is the
|
|
4832
|
-
* profile's to rewrite. `adopted` — the row was already here under a name a person chose (they ran `client add` with
|
|
4833
|
-
* the organisation's file), and stays theirs: no `org` command changes or removes it. `serves` is per generation,
|
|
4834
|
-
* because it is a fact about one client, not about the organisation.
|
|
4835
|
-
*/
|
|
4836
|
-
interface OrganisationGeneration {
|
|
4837
|
-
name: string;
|
|
4838
|
-
clientId: string;
|
|
4839
|
-
projectId?: string | undefined;
|
|
4840
|
-
ownership: "owned" | "adopted";
|
|
4841
|
-
serves: OrganisationServes;
|
|
4842
|
-
addedAt: string;
|
|
4843
|
-
}
|
|
4844
|
-
interface OrganisationSlackApp {
|
|
4845
|
-
clientId: string;
|
|
4846
|
-
appId?: string | undefined;
|
|
4847
|
-
}
|
|
4848
|
-
/**
|
|
4849
|
-
* An organisation's update caps, as the file beside its profile names them (design 2026-10-16, U8), and the SHA-256 of
|
|
4850
|
-
* that file, so `org update` can tell the organisation changing them from the record drifting. A `max` word this release
|
|
4851
|
-
* does not know reads as `never`, the strictest it could mean: caps only ever tighten.
|
|
4852
|
-
*/
|
|
4853
|
-
interface OrganisationUpdates {
|
|
4854
|
-
max?: string | undefined;
|
|
4855
|
-
background?: boolean | undefined;
|
|
4856
|
-
sha256: string;
|
|
4857
|
-
}
|
|
4858
|
-
/**
|
|
4859
|
-
* The organisation's bot-only agent app, as its profile's agent-app file names it (CUE-493): the app's id, and the
|
|
4860
|
-
* SHA-256 of that file, so `org update` can tell the organisation changing it from the record drifting. No client id:
|
|
4861
|
-
* nobody signs in through it — its administrator installs it once and its members paste its bot token.
|
|
4862
|
-
*/
|
|
4863
|
-
interface OrganisationAgentApp {
|
|
4864
|
-
appId: string;
|
|
4865
|
-
sha256: string;
|
|
4866
|
-
}
|
|
4867
|
-
/**
|
|
4868
|
-
* What this machine records of one organisation profile: never its bytes and never its secret — where it was read
|
|
4869
|
-
* from, the SHA-256 of what was read and when, the generations of its Google client, and its Slack apps.
|
|
4870
|
-
*/
|
|
4871
|
-
interface OrganisationRecord {
|
|
4872
|
-
label: string;
|
|
4873
|
-
/** Read again, from this absolute path, by every `org update`. `file` is the one kind version 1 knows (§D3). */
|
|
4874
|
-
source: {
|
|
4875
|
-
kind: string;
|
|
4876
|
-
path: string;
|
|
4877
|
-
};
|
|
4878
|
-
sha256: string;
|
|
4879
|
-
readAt: string;
|
|
4880
|
-
addedAt: string;
|
|
4881
|
-
/** Whether the member asked for this organisation's client to serve addresses outside it too (§D6). */
|
|
4882
|
-
forOtherAddresses: boolean;
|
|
4883
|
-
/** `active` names the generation new mailboxes get — none once a profile stops naming a Google client. */
|
|
4884
|
-
gmail?: {
|
|
4885
|
-
active: string | null;
|
|
4886
|
-
generations: OrganisationGeneration[];
|
|
4887
|
-
} | undefined;
|
|
4888
|
-
slack?: {
|
|
4889
|
-
workspace: string;
|
|
4890
|
-
workspaceName: string;
|
|
4891
|
-
redirectPort: number;
|
|
4892
|
-
apps: {
|
|
4893
|
-
read?: OrganisationSlackApp | undefined;
|
|
4894
|
-
send?: OrganisationSlackApp | undefined;
|
|
4895
|
-
agent?: OrganisationAgentApp | undefined;
|
|
4896
|
-
};
|
|
4897
|
-
} | undefined;
|
|
4898
|
-
/** The organisation's update caps, from the file beside its profile; absent when it names none. */
|
|
4899
|
-
updates?: OrganisationUpdates | undefined;
|
|
4568
|
+
interface AuditRecord {
|
|
4569
|
+
at: string;
|
|
4570
|
+
/** Immutable inbox id; `alias` is the label at the time. */
|
|
4571
|
+
inboxId: string;
|
|
4572
|
+
alias?: string;
|
|
4573
|
+
operation: string;
|
|
4574
|
+
/** `started` marks the first half of a change recorded before it is made; a later record for the same ids ends it. */
|
|
4575
|
+
outcome: "ok" | "refused" | "failed" | "started";
|
|
4576
|
+
ids?: Record<string, string | string[] | CondensedIds>;
|
|
4577
|
+
recipientDomains?: string[];
|
|
4900
4578
|
/**
|
|
4901
|
-
*
|
|
4902
|
-
*
|
|
4903
|
-
*
|
|
4904
|
-
*
|
|
4905
|
-
* Held as the file holds it, whatever its shape, and read only through `ignoredListOf`, which keeps the well-formed
|
|
4906
|
-
* entries and names the rest: 0.21.0 is the first release to read it, so a shape a later release or a hand gave it
|
|
4907
|
-
* must never make the whole configuration unreadable, nor be dropped by an unrelated write. Losing it is safe: a
|
|
4908
|
-
* later release that understands an entry judges applying it by its classifier whether or not the list names it —
|
|
4909
|
-
* a tightening at once, anything else approved first, never a repair of drift (X4).
|
|
4579
|
+
* Full canonical recipient addresses. Only a send writes these: for every other operation the domains are enough,
|
|
4580
|
+
* and storing less is better — but when mail has actually left, "who did it go to" is the first question anyone
|
|
4581
|
+
* asks afterwards, and a domain does not answer it.
|
|
4910
4582
|
*/
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4583
|
+
recipients?: string[];
|
|
4584
|
+
approvalId?: string;
|
|
4585
|
+
reason?: string;
|
|
4586
|
+
/** The process that performed the work; daemon work keeps its caller separately in `origin`. */
|
|
4587
|
+
surface?: "cli" | "mcp" | "app" | "daemon";
|
|
4588
|
+
/** The requesting client surface; autonomous work is `daemon`/`daemon`. */
|
|
4589
|
+
origin?: "cli" | "mcp" | "app" | "daemon";
|
|
4916
4590
|
/**
|
|
4917
|
-
* The
|
|
4918
|
-
*
|
|
4919
|
-
*
|
|
4591
|
+
* The policy that decided how this was approved. Written by change approvals, where it is the whole question
|
|
4592
|
+
* afterwards: a loosening a person typed a code for and one an agent claimed after a yes in chat look the same in
|
|
4593
|
+
* the configuration, and only this line tells them apart.
|
|
4920
4594
|
*/
|
|
4921
|
-
|
|
4922
|
-
}
|
|
4923
|
-
/** What a legacy drain records of one tracked record: still open, or what became of it. */
|
|
4924
|
-
type LegacyDrainOutcome = "revoked" | "expired" | "used" | "failed" | "sending" | "unknown";
|
|
4925
|
-
/**
|
|
4926
|
-
* The approvals an earlier release prepared that were still waiting, or already being sent, when this configuration
|
|
4927
|
-
* became version 3, and what has become of each (design 2026-10-05 §D1, "Old releases are locked out before the fence
|
|
4928
|
-
* is relied on").
|
|
4929
|
-
*/
|
|
4930
|
-
interface LegacyDrain {
|
|
4931
|
-
/** When the conversion that opened the drain was written. */
|
|
4932
|
-
since: string;
|
|
4595
|
+
policy?: SendPolicy;
|
|
4933
4596
|
/**
|
|
4934
|
-
*
|
|
4935
|
-
*
|
|
4597
|
+
* What daily retention deleted (`operation: 'approval.retained'`, design 2026-10-05 §D9): the approval's kind, its
|
|
4598
|
+
* final state, when it finished and any provider id — never its content or a recipient. Appended durably before the
|
|
4599
|
+
* record is unlinked, so the audit log keeps the terminal history the approval directory no longer does.
|
|
4936
4600
|
*/
|
|
4937
|
-
|
|
4601
|
+
retained?: RetainedApproval;
|
|
4938
4602
|
}
|
|
4939
|
-
/**
|
|
4940
|
-
interface
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
legacyDrain?: LegacyDrain | undefined;
|
|
4603
|
+
/** Large id lists are condensed so every audit line stays small enough to be appended atomically. */
|
|
4604
|
+
interface CondensedIds {
|
|
4605
|
+
count: number;
|
|
4606
|
+
sha256: string;
|
|
4607
|
+
first: string[];
|
|
4945
4608
|
}
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4609
|
+
declare class AuditLog {
|
|
4610
|
+
#private;
|
|
4611
|
+
readonly directory: string;
|
|
4612
|
+
constructor(stateDir: string, now?: () => Date);
|
|
4613
|
+
append(record: Omit<AuditRecord, "at"> & {
|
|
4614
|
+
at?: string;
|
|
4615
|
+
}, options?: {
|
|
4616
|
+
durable?: boolean;
|
|
4617
|
+
}): Promise<AuditRecord>;
|
|
4618
|
+
/** The most recent records, newest last, optionally filtered by inbox and a lower time bound. */
|
|
4619
|
+
tail(options?: {
|
|
4620
|
+
limit?: number;
|
|
4621
|
+
inbox?: string;
|
|
4622
|
+
since?: string;
|
|
4623
|
+
}): Promise<AuditRecord[]>;
|
|
4950
4624
|
}
|
|
4951
|
-
/**
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4625
|
+
/** The step of a batch that failed. */
|
|
4626
|
+
type MaintenanceStep = "lock" | "state" | "enumerate" | "stat" | "read" | "write" | "append" | "claim-unlink" | "record-unlink" | "lock-lost" | "cursor";
|
|
4627
|
+
/** A failure a batch met: which step, for which record, and the error's code — never anything a record holds. */
|
|
4628
|
+
interface MaintenanceError {
|
|
4629
|
+
readonly step: MaintenanceStep;
|
|
4630
|
+
readonly approvalId?: string | undefined;
|
|
4631
|
+
readonly code: string;
|
|
4955
4632
|
}
|
|
4956
4633
|
/**
|
|
4957
|
-
*
|
|
4958
|
-
*
|
|
4634
|
+
* What one call of `ensurePruned` did. `attempted` — it ran a batch; when it did not, `skipped` says why: not due
|
|
4635
|
+
* (a batch ran less than a day ago), busy (another process holds the maintenance lock), or no records (there is no
|
|
4636
|
+
* approvals directory at all). `processed` counts the slots whose record work ran to its end, `pruned` the records
|
|
4637
|
+
* deleted, `skippedBusy` the records whose lock was held elsewhere and were left. `complete` — the batch reached the end
|
|
4638
|
+
* of the directory inside both bounds; hitting 200 slots or the deadline, or losing the lock, leaves it false.
|
|
4959
4639
|
*/
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
load(): Promise<Config$1>;
|
|
4982
|
-
/**
|
|
4983
|
-
* Read-modify-write under a lock, so a CLI command and a running MCP server never lose each other's changes. The
|
|
4984
|
-
* mutator receives a fresh copy read inside the lock and returns the new config, which is validated before writing.
|
|
4985
|
-
*/
|
|
4986
|
-
update(mutator: (config: Config$1) => Config$1 | Promise<Config$1>, options?: {
|
|
4987
|
-
consent?: LooseningConsent;
|
|
4988
|
-
signal?: AbortSignal | undefined;
|
|
4989
|
-
}): Promise<ConfigUpdateResult>;
|
|
4990
|
-
/**
|
|
4991
|
-
* The one way a version-1 config becomes version 2.
|
|
4992
|
-
*
|
|
4993
|
-
* Under the credentials lock and then the config lock — the order everything takes them in — so it cannot land
|
|
4994
|
-
* between a removal's read and its write, or in the middle of moving secrets between backends.
|
|
4995
|
-
*
|
|
4996
|
-
* `expected` is the fingerprint of the config the caller previewed. The file is read again inside the locks, and
|
|
4997
|
-
* if it is not that config any more the whole thing is refused: somebody confirmed a mapping computed from
|
|
4998
|
-
* something else. Nothing waits for a person while holding a lock; the preview happens before this is called.
|
|
4999
|
-
*
|
|
5000
|
-
* `build` produces version 2 from the locked snapshot. What it may change is checked rather than trusted: the same
|
|
5001
|
-
* accounts, byte for byte, under new keys — a rename grants nothing, so a build that changed anything else is a
|
|
5002
|
-
* bug and is refused before it is written.
|
|
5003
|
-
*
|
|
5004
|
-
* Idempotent, but only for this plan. A retry after a write that committed — even one whose lock release then
|
|
5005
|
-
* failed — finds version 2, recognises its own mapping in it, and says so. Version 2 that does *not* carry this
|
|
5006
|
-
* mapping is somebody else's migration; saying "already migrated" there would report a mapping nobody applied,
|
|
5007
|
-
* and a caller updating registrations from it would point them at names that do not exist.
|
|
5008
|
-
*
|
|
5009
|
-
* `rows` is the plan, and the question is asked of the mapping rather than of the whole file: between a
|
|
5010
|
-
* committed write and its retry, something else may have changed a policy or a timezone, and a retry refused
|
|
5011
|
-
* over that would be idempotency in name only. The rows are checked here rather than by whoever built them,
|
|
5012
|
-
* for the same reason `build` is: a caller that could answer its own question could answer it wrongly.
|
|
5013
|
-
*
|
|
5014
|
-
* The file it replaces is copied beside it first — see `backUpBeforeMigration` — and the copy's path returned.
|
|
5015
|
-
*/
|
|
5016
|
-
migrateNames(expected: string, rows: readonly RenamedAccount[], build: (current: PlainConfig) => NamedConfig): Promise<{
|
|
5017
|
-
status: "migrated" | "already-migrated";
|
|
5018
|
-
config: NamedConfig;
|
|
5019
|
-
backup?: string;
|
|
5020
|
-
}>;
|
|
4640
|
+
interface MaintenanceStatus {
|
|
4641
|
+
readonly attempted: boolean;
|
|
4642
|
+
readonly processed: number;
|
|
4643
|
+
readonly pruned: number;
|
|
4644
|
+
readonly skippedBusy: number;
|
|
4645
|
+
readonly complete: boolean;
|
|
4646
|
+
readonly errors: readonly MaintenanceError[];
|
|
4647
|
+
readonly skipped?: "not-due" | "busy" | "no-records" | undefined;
|
|
4648
|
+
}
|
|
4649
|
+
/** What a batch reports of a record it is about to delete: never a body, an address, or anything a sender wrote. */
|
|
4650
|
+
interface RetainedApproval {
|
|
4651
|
+
readonly kind: ApprovalKind;
|
|
4652
|
+
readonly state: ApprovalState;
|
|
4653
|
+
/** When it finished: its validated terminal time, or a legacy used record's safe `updatedAt`. */
|
|
4654
|
+
readonly finishedAt: string;
|
|
4655
|
+
/** A used send's provider id, when it has a non-empty one. */
|
|
4656
|
+
readonly providerId?: string | undefined;
|
|
4657
|
+
/** A record an earlier release wrote. */
|
|
4658
|
+
readonly legacy?: true | undefined;
|
|
4659
|
+
}
|
|
4660
|
+
interface MaintenanceOptions {
|
|
5021
4661
|
/**
|
|
5022
|
-
*
|
|
5023
|
-
*
|
|
5024
|
-
*
|
|
5025
|
-
* 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
|
|
5026
|
-
* 0.13 process can start a prepare, a claim, an approval or a policy change through this file. Nothing is renamed:
|
|
5027
|
-
* a version-1 body stays version 1's (`naming: 1`) until the names migration, which needs its own approval.
|
|
5028
|
-
*
|
|
5029
|
-
* Under the config lock, the file is read again — never from the cache, never from the read before the lock — and an
|
|
5030
|
-
* existing version 3 returned unchanged: a second converter that lost the race keeps the winner's epochs and drain.
|
|
5031
|
-
* `scan` runs under the same lock, so a record written while this waited for it is seen, and an update cannot land
|
|
5032
|
-
* between the scan and the write; the version-3 write carries `legacyDrain` only when the scan tracked something.
|
|
5033
|
-
* With no config file at all there is nothing to convert, and nothing is written.
|
|
4662
|
+
* When no further step may start, on the store's clock, in milliseconds: five seconds from the call when left out.
|
|
4663
|
+
* A report passes the one deadline it shares with its own reads.
|
|
5034
4664
|
*/
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
4665
|
+
readonly deadline?: number | undefined;
|
|
4666
|
+
}
|
|
4667
|
+
//#endregion
|
|
4668
|
+
//#region src/approval-validate.d.ts
|
|
4669
|
+
/**
|
|
4670
|
+
* The integrity check of a version-2 approval record (design 2026-10-05 §D1, "Digest integrity" and "Version-2
|
|
4671
|
+
* timestamps fail closed"). Pure: it reads the record and the id its file is named by, and decides.
|
|
4672
|
+
*
|
|
4673
|
+
* A record either passes, or is `corrupt` for one fixed reason — never a sentence built from the record, so nothing a
|
|
4674
|
+
* file holds is ever echoed — and of one of two attribution classes. `verified`: its `bindingDigest` recomputes, so its
|
|
4675
|
+
* owner and identity can be trusted and it is corrupt for another reason (a state, a timestamp, its evidence).
|
|
4676
|
+
* `unverifiable`: its binding is missing, malformed or does not match, or it sits in another record's file, so nothing
|
|
4677
|
+
* it says about whom it belongs to can be trusted.
|
|
4678
|
+
*
|
|
4679
|
+
* Nothing here is wired into a read yet; the store's decoder does that.
|
|
4680
|
+
*/
|
|
4681
|
+
/** Why a version-2 record is corrupt: a fixed category, never the record's own words. */
|
|
4682
|
+
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" | "narrowing-invalid" | "state-impossible" | "lifetime-mismatch" | "timestamp-invalid" | "timestamp-missing" | "timestamp-misplaced" | "timestamp-misordered" | "sent-id-missing" | "evidence-missing" | "evidence-invalid" | "evidence-contradictory";
|
|
4683
|
+
type Attribution = "verified" | "unverifiable";
|
|
4684
|
+
type ErrorCode = "UNEXPECTED" | "APPROVAL_REQUIRED" | "APPROVAL_PENDING" | "APPROVAL_EXPIRED" | "APPROVAL_VOID" | "SEND_OUTCOME_UNKNOWN" | "SEND_REFUSED" | "POLICY_NEVER" | "ACT_NOT_ALLOWED" | "STANDING_REFUSED" | "STANDING_LIMIT" | "RATE_CAPPED" | "UNSENDABLE_HTML" | "LOOSENING_REFUSED" | "NEEDS_NEWER_RELEASE" | "USAGE" | "CURSOR_MISMATCH" | "BAD_DATA" | "REPLY_INVALID" | "NOT_FOUND" | "PROVIDER_UNAVAILABLE" | "SOURCE_UNAVAILABLE" | "SECRET_STORE_UNAVAILABLE" | "TRANSIENT" | "KEYCHAIN_APPROVAL_PENDING" | "LOCK_TIMEOUT" | "AUTH_REQUIRED" | "SCOPE_MISSING" | "WHATSAPP_VISIBILITY_SEAM_REQUIRED" | "CONFIG";
|
|
4685
|
+
interface CommsErrorOptions {
|
|
4686
|
+
hint?: string;
|
|
4687
|
+
details?: Record<string, unknown>;
|
|
4688
|
+
cause?: unknown;
|
|
5041
4689
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
readonly
|
|
5045
|
-
readonly
|
|
5046
|
-
|
|
5047
|
-
|
|
4690
|
+
declare class CommsError extends Error {
|
|
4691
|
+
readonly code: ErrorCode;
|
|
4692
|
+
readonly hint: string | undefined;
|
|
4693
|
+
readonly details: Record<string, unknown> | undefined;
|
|
4694
|
+
constructor(code: ErrorCode, message: string, options?: CommsErrorOptions);
|
|
4695
|
+
get exitCode(): number;
|
|
4696
|
+
get retryable(): boolean;
|
|
5048
4697
|
}
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
readonly [key: string]: unknown;
|
|
5052
|
-
} | null;
|
|
4698
|
+
//#endregion
|
|
4699
|
+
//#region src/approval-stored.d.ts
|
|
5053
4700
|
/**
|
|
5054
|
-
*
|
|
4701
|
+
* Every approval file, read one way (design 2026-10-05 §D2, "Legacy version-1 records, one decoder everywhere" and
|
|
4702
|
+
* "Two classes of parseable corrupt record").
|
|
5055
4703
|
*
|
|
5056
|
-
*
|
|
5057
|
-
*
|
|
5058
|
-
*
|
|
4704
|
+
* A file is one of four things, and every read says which:
|
|
4705
|
+
*
|
|
4706
|
+
* - `v2` — a version-2 record that passed `validateV2`: the only form anything may act on;
|
|
4707
|
+
* - `legacy` — a structurally valid version-1 record, read by its own release's rules: reported, never claimed;
|
|
4708
|
+
* - `corrupt` — a parseable version-2 record that failed its integrity check, attributable to its owner or not;
|
|
4709
|
+
* - `unreadable` — a file that cannot safely be read at all: shown only as a stub naming its id (from the file name)
|
|
4710
|
+
* and a fixed reason, never anything the file holds.
|
|
4711
|
+
*
|
|
4712
|
+
* Nothing is silently omitted, and the decoder writes nothing.
|
|
5059
4713
|
*/
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
readonly
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
readonly
|
|
4714
|
+
/** Why a file cannot safely be read: a fixed category, never the file's own bytes. */
|
|
4715
|
+
type UnreadableReason = "invalid-json" | "truncated" | "missing-ownership" | "missing-kind" | "wrong-shape" | "file-name-mismatch";
|
|
4716
|
+
/** What may be said of a record that cannot be trusted: its id, that it is corrupt, and why — nothing else. */
|
|
4717
|
+
interface CorruptStub {
|
|
4718
|
+
readonly approvalId: string;
|
|
4719
|
+
readonly state: "corrupt";
|
|
4720
|
+
readonly reason: UnreadableReason | IntegrityReason;
|
|
4721
|
+
}
|
|
4722
|
+
/** What an attribution-verified corrupt record may still show its owner: the fields its binding was made over. */
|
|
4723
|
+
interface OrdinarySafeFields {
|
|
4724
|
+
readonly kind: ApprovalKind;
|
|
4725
|
+
readonly channel: string;
|
|
4726
|
+
readonly ownerScope: OwnerScope;
|
|
4727
|
+
readonly inboxId: string;
|
|
4728
|
+
readonly inboxSub?: string | undefined;
|
|
4729
|
+
readonly draftId: string;
|
|
4730
|
+
readonly draftMessageId: string;
|
|
4731
|
+
readonly expect: Expectation;
|
|
4732
|
+
}
|
|
4733
|
+
/** A disclosure record has no channel owner or sender-controlled operational fields to expose. */
|
|
4734
|
+
interface DisclosureSafeFields {
|
|
4735
|
+
readonly kind: "disclosure";
|
|
5071
4736
|
}
|
|
4737
|
+
type SafeFields = OrdinarySafeFields | DisclosureSafeFields;
|
|
4738
|
+
type StoredApproval = {
|
|
4739
|
+
readonly form: "v2";
|
|
4740
|
+
readonly record: ApprovalRecord;
|
|
4741
|
+
} | {
|
|
4742
|
+
readonly form: "legacy";
|
|
4743
|
+
readonly view: LegacyView;
|
|
4744
|
+
readonly record: LegacyApprovalRecord;
|
|
4745
|
+
} | {
|
|
4746
|
+
readonly form: "corrupt";
|
|
4747
|
+
readonly approvalId: string;
|
|
4748
|
+
readonly reason: IntegrityReason;
|
|
4749
|
+
readonly attribution: Attribution;
|
|
4750
|
+
/** Only when attribution is verified. */
|
|
4751
|
+
readonly safe: SafeFields | null;
|
|
4752
|
+
} | {
|
|
4753
|
+
readonly form: "unreadable";
|
|
4754
|
+
readonly stub: CorruptStub;
|
|
4755
|
+
};
|
|
4756
|
+
//#endregion
|
|
4757
|
+
//#region src/approval-outcome.d.ts
|
|
5072
4758
|
/**
|
|
5073
|
-
* One
|
|
4759
|
+
* One locked classification of every record into an outcome (design 2026-10-05 §D2).
|
|
5074
4760
|
*
|
|
5075
|
-
*
|
|
5076
|
-
*
|
|
5077
|
-
*
|
|
4761
|
+
* `approvalOutcome` says what a record is now — its state, whether it can be claimed, and what an action on it would be
|
|
4762
|
+
* told — from the record as it was read under its own lock and the configuration read in that same lock (`LiveGate`).
|
|
4763
|
+
* Nothing here reads a file or a clock of its own, and nothing here writes: the store persists what the outcome
|
|
4764
|
+
* derives, and only from inside the lock it was classified under.
|
|
5078
4765
|
*/
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
readonly id?: string | undefined;
|
|
5084
|
-
}
|
|
4766
|
+
/** What is being done with the record: an approval, a claim, a wait, a revoke, or a look. */
|
|
4767
|
+
type OutcomeAction = "approve" | "claim" | "wait" | "revoke" | "inspect";
|
|
4768
|
+
/** A record's state as anyone may be told it: a download's answered question says `answered`. */
|
|
4769
|
+
type PublicApprovalState = ApprovalState | "corrupt" | "answered";
|
|
5085
4770
|
/**
|
|
5086
|
-
*
|
|
5087
|
-
*
|
|
4771
|
+
* Where an approval stands, as every surface may show it (design 2026-10-05 §D8): its id, kind, channel, state and
|
|
4772
|
+
* whether it can be claimed, with the timestamps its state carries. Never a challenge hash, and nothing a sender wrote.
|
|
5088
4773
|
*/
|
|
5089
|
-
interface
|
|
5090
|
-
|
|
5091
|
-
|
|
4774
|
+
interface ApprovalObject {
|
|
4775
|
+
readonly id: string;
|
|
4776
|
+
readonly kind: ApprovalKind | null;
|
|
4777
|
+
readonly channel: string | null;
|
|
4778
|
+
readonly state: PublicApprovalState;
|
|
4779
|
+
readonly claimable: boolean;
|
|
4780
|
+
readonly route?: ApprovalRoute | undefined;
|
|
4781
|
+
readonly ownerRemoved?: true | undefined;
|
|
4782
|
+
/** A record from an earlier release, read by its own rules: shown, never claimed. */
|
|
4783
|
+
readonly legacy?: true | undefined;
|
|
4784
|
+
readonly reason?: string | undefined;
|
|
4785
|
+
readonly createdAt?: string | undefined;
|
|
4786
|
+
readonly expiresAt?: string | undefined;
|
|
4787
|
+
readonly approvedAt?: string | undefined;
|
|
4788
|
+
readonly usableUntil?: string | undefined;
|
|
4789
|
+
readonly sendingAt?: string | undefined;
|
|
4790
|
+
readonly sendingHeartbeatAt?: string | undefined;
|
|
4791
|
+
/** When a `sending` record reads `unknown` (`unknownAtOf`): derived, never stored. Also on an `unknown` one. */
|
|
4792
|
+
readonly unknownAt?: string | undefined;
|
|
4793
|
+
readonly usedAt?: string | undefined;
|
|
4794
|
+
readonly sentAt?: string | undefined;
|
|
4795
|
+
readonly sentMessageId?: string | undefined;
|
|
4796
|
+
readonly failedAt?: string | undefined;
|
|
4797
|
+
readonly revokedAt?: string | undefined;
|
|
4798
|
+
readonly expiredAt?: string | undefined;
|
|
4799
|
+
}
|
|
4800
|
+
/** What an action on a record is, now. */
|
|
4801
|
+
interface ApprovalOutcome {
|
|
4802
|
+
readonly state: PublicApprovalState;
|
|
4803
|
+
readonly claimable: boolean;
|
|
4804
|
+
readonly ownerRemoved?: true | undefined;
|
|
4805
|
+
readonly reason?: string | undefined;
|
|
5092
4806
|
/**
|
|
5093
|
-
*
|
|
5094
|
-
*
|
|
5095
|
-
* A terminal challenge is answered moments before the write it permits, so the paths are enough. An approval is
|
|
5096
|
-
* prepared first and applied later — after a sign-in, possibly minutes later — and in between the configuration
|
|
5097
|
-
* can move underneath it: the same path, loosened from a different value, or on a different account that took
|
|
5098
|
-
* the name. With these, `ConfigStore.update` refuses any loosening that is not exactly one of them.
|
|
4807
|
+
* A version-2 record as this classification reads it: with a revocation it derived applied (an owner removed, a
|
|
4808
|
+
* stale send epoch, or — for a claim or an approval — a live `never`). Null for every other form.
|
|
5099
4809
|
*/
|
|
5100
|
-
|
|
4810
|
+
readonly record: ApprovalRecord | null;
|
|
4811
|
+
/** Whether `record` carries a revocation the classification derived, for a locked action to persist. */
|
|
4812
|
+
readonly revokes: boolean;
|
|
4813
|
+
/** Where it stands, as D8 shows it (`publicApproval`). */
|
|
4814
|
+
readonly approval: ApprovalObject;
|
|
4815
|
+
/** What the action is refused with, when it is refused by the record's state alone. */
|
|
4816
|
+
readonly error?: CommsError | undefined;
|
|
5101
4817
|
}
|
|
4818
|
+
/**
|
|
4819
|
+
* What became of a used send, in the words of the surface that says it, from the time the provider accepted it (D2's
|
|
4820
|
+
* used-send row): Gmail and Slack, where acceptance is sending, say it was "sent at …"; Resend's own surfaces that it
|
|
4821
|
+
* was "accepted by Resend at …"; core's generic surfaces "accepted by <provider> at …".
|
|
4822
|
+
*/
|
|
4823
|
+
type UsedSaid = (usedAt: string) => string;
|
|
5102
4824
|
//#endregion
|
|
5103
|
-
//#region src/
|
|
4825
|
+
//#region src/handoff-text.d.ts
|
|
5104
4826
|
/**
|
|
5105
|
-
*
|
|
5106
|
-
*
|
|
5107
|
-
* A record stores two digests. `contentDigest` is the outward content or change, computed as before: the message, the
|
|
5108
|
-
* change, the download. `bindingDigest` is this module's: the content digest together with the record's route, its
|
|
5109
|
-
* lifetimes and its own identity — who it belongs to, which draft, which channel, which send epoch. Every one of those
|
|
5110
|
-
* is a field a claim or a report acts on, so a record whose binding no longer recomputes from its own fields has been
|
|
5111
|
-
* altered since it was made, and nothing it says can be trusted.
|
|
4827
|
+
* A handoff as words a person reads, and what a core function that prints one is given (CUE-403; CONTRIBUTING.md,
|
|
4828
|
+
* "Telling a person what to run"). `handoffs.ts` makes located handoffs and re-exports all of this.
|
|
5112
4829
|
*
|
|
5113
|
-
*
|
|
5114
|
-
* from them
|
|
4830
|
+
* Kept apart from the locator on purpose: the configuration store, the secret stores and the attachment jail print
|
|
4831
|
+
* handoffs too, and the locator's own imports reach the configuration store, so importing it from them would be a
|
|
4832
|
+
* cycle. Nothing here locates anything or imports a module that does: a located command is made by `handoffs.ts` and
|
|
4833
|
+
* handed in, and what is here only renders one. With none handed in, there is nothing to render: a package that did not
|
|
4834
|
+
* give core its caller and is asked for a command has a programming error, not a bare name to fall back on.
|
|
5115
4835
|
*/
|
|
4836
|
+
/** A command for a person to run, located, or the sentence saying why there is none here. */
|
|
4837
|
+
type Handoff = PrintedCommand | CliCommandNotLocated;
|
|
4838
|
+
interface HandoffUse {
|
|
4839
|
+
/** The suite folders the command reads or writes; the four every command opens when left out. */
|
|
4840
|
+
readonly uses?: readonly PathName[] | undefined;
|
|
4841
|
+
/** It also reads or writes downloads, so the downloads folder is pinned too. */
|
|
4842
|
+
readonly downloads?: boolean | undefined;
|
|
4843
|
+
}
|
|
5116
4844
|
/**
|
|
5117
|
-
*
|
|
5118
|
-
*
|
|
5119
|
-
*
|
|
4845
|
+
* The commands a package tells a person to run, made once per printing process from its caller (`handoffs.ts`,
|
|
4846
|
+
* `cliHandoffs`): `openCore({ caller })` puts them on `core.handoffs`. Each method takes the CLI's own words, never the
|
|
4847
|
+
* program, and returns a `Handoff`.
|
|
5120
4848
|
*/
|
|
5121
|
-
|
|
5122
|
-
/** The
|
|
5123
|
-
|
|
5124
|
-
/**
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
readonly
|
|
5128
|
-
|
|
5129
|
-
readonly
|
|
4849
|
+
interface CliHandoffs {
|
|
4850
|
+
/** The printing package: one of its own modules and its name. */
|
|
4851
|
+
readonly caller: CliCommandCaller;
|
|
4852
|
+
/** The shell the commands are quoted for. */
|
|
4853
|
+
readonly platform: NodeJS.Platform;
|
|
4854
|
+
/** The printing package's own CLI, with these words after the program. */
|
|
4855
|
+
own(words: readonly string[], use?: HandoffUse): Handoff;
|
|
4856
|
+
/** The core CLI: the printing package's own when that is core, the core it has installed otherwise. */
|
|
4857
|
+
core(words: readonly string[], use?: HandoffUse): Handoff;
|
|
4858
|
+
/** The CLI of `channel` — `core`, `gmail`, `slack` …: own, core, or another product found among its registrations. */
|
|
4859
|
+
of(channel: string, words: readonly string[], use?: HandoffUse): Handoff;
|
|
4860
|
+
/** The same commands, quoted for another shell. */
|
|
4861
|
+
on(platform: NodeJS.Platform): CliHandoffs;
|
|
4862
|
+
/**
|
|
4863
|
+
* The same, able to find another product: with the servers registered with this machine's MCP clients read now, from
|
|
4864
|
+
* the environment these were made with. Read once per call; nothing a registration names is run.
|
|
4865
|
+
*/
|
|
4866
|
+
registered(): Promise<CliHandoffs>;
|
|
4867
|
+
/** The same, finding another product among these registrations: a scan the caller has already read. */
|
|
4868
|
+
withRegistrations(registrations: readonly RegisteredServer[]): CliHandoffs;
|
|
5130
4869
|
}
|
|
5131
4870
|
/**
|
|
5132
|
-
*
|
|
5133
|
-
*
|
|
5134
|
-
* The event daemon derives this from one canonical activation document. Core does not construct
|
|
5135
|
-
* that document; it verifies the closed shape that makes an independently supplied version list
|
|
5136
|
-
* impossible to smuggle into a disclosure record.
|
|
4871
|
+
* Why a saved name has {@link DOWNLOAD_SUFFIX} after it: a name tools read or run on their own (`auto-read`), an
|
|
4872
|
+
* extension that is not one of the inert ones (`type`), or no extension at all (`no-extension`).
|
|
5137
4873
|
*/
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
4874
|
+
type RenameReason = "auto-read" | "type" | "no-extension";
|
|
4875
|
+
//#endregion
|
|
4876
|
+
//#region src/ledger.d.ts
|
|
4877
|
+
interface Caps {
|
|
4878
|
+
perHour: number;
|
|
4879
|
+
perDay: number;
|
|
4880
|
+
}
|
|
4881
|
+
interface CapStatus {
|
|
4882
|
+
hour: number;
|
|
4883
|
+
day: number;
|
|
4884
|
+
/** When the next slot frees up, if a cap is reached. */
|
|
4885
|
+
resetAt?: string;
|
|
4886
|
+
}
|
|
4887
|
+
declare class SendLedger {
|
|
4888
|
+
#private;
|
|
4889
|
+
readonly directory: string;
|
|
4890
|
+
constructor(stateDir: string, now?: () => Date);
|
|
4891
|
+
/** Counts sends (including in-flight reservations) in the last hour and day. */
|
|
4892
|
+
status(inboxId: string, caps: Caps): Promise<CapStatus>;
|
|
4893
|
+
/** Reserves a slot or refuses with APPROVAL_REQUIRED and the time the cap resets. Atomic across processes. */
|
|
4894
|
+
reserve(inboxId: string, approvalId: string, caps: Caps): Promise<CapStatus>;
|
|
4895
|
+
/** Releases a reservation whose send did not happen. */
|
|
4896
|
+
release(inboxId: string, approvalId: string): Promise<void>;
|
|
5143
4897
|
}
|
|
5144
|
-
/**
|
|
5145
|
-
* What a record's `inboxId` names. `owner` — a mailbox or account that existed when it was made (every send and
|
|
5146
|
-
* download, and a change to one). `prospective` — a change that creates its owner, which has no id yet. `global` — a
|
|
5147
|
-
* change to no owner at all, stored with `inboxId: ''`.
|
|
5148
|
-
*/
|
|
5149
|
-
type OwnerScope = "owner" | "prospective" | "global";
|
|
5150
4898
|
//#endregion
|
|
5151
|
-
//#region src/
|
|
4899
|
+
//#region src/plans.d.ts
|
|
5152
4900
|
/**
|
|
5153
|
-
*
|
|
5154
|
-
*
|
|
5155
|
-
*
|
|
5156
|
-
* was opened with others.
|
|
5157
|
-
*
|
|
5158
|
-
* Only these two passes go through it: every other read and write of the store is as it was.
|
|
4901
|
+
* Bulk mailbox changes (more than 20 messages, anything driven by a query, and every trash) are two-step: a dry run
|
|
4902
|
+
* returns a plan token bound to the exact set of ids and the exact change; executing requires that token, once, within
|
|
4903
|
+
* ten minutes. The ids are resolved at dry-run time, so a query cannot silently grow between preview and execution.
|
|
5159
4904
|
*/
|
|
5160
|
-
interface
|
|
5161
|
-
|
|
5162
|
-
readdir(directory: string): Promise<string[]>;
|
|
5163
|
-
/** One metadata stat: the modification time, in milliseconds. */
|
|
5164
|
-
stat(path: string): Promise<{
|
|
5165
|
-
readonly mtimeMs: number;
|
|
5166
|
-
}>;
|
|
5167
|
-
/** One open of a file's content, as text. */
|
|
5168
|
-
readText(path: string): Promise<string>;
|
|
5169
|
-
/** Replaces a file atomically — a temporary file, flushed, renamed — and, when `durable`, syncs its directory too. */
|
|
5170
|
-
writeAtomic(path: string, text: string, options?: {
|
|
5171
|
-
durable?: boolean;
|
|
5172
|
-
}): Promise<void>;
|
|
5173
|
-
/** One unlink of an approval artifact: a record or its claim marker. A missing one rejects with `ENOENT`. */
|
|
5174
|
-
unlink(path: string): Promise<void>;
|
|
5175
|
-
/** The lock files' own operations (`tryFileLock`). */
|
|
5176
|
-
readonly lock: LockIo;
|
|
5177
|
-
}
|
|
5178
|
-
/** A version-1 record, as 0.13.0 wrote it: a send has no `kind`, and one `digest` stands for the content. */
|
|
5179
|
-
interface LegacyApprovalRecord {
|
|
5180
|
-
approvalId: string;
|
|
5181
|
-
/** Absent on a send. */
|
|
5182
|
-
kind?: ApprovalKind | undefined;
|
|
5183
|
-
digestVersion: 1;
|
|
4905
|
+
interface PlanRecord {
|
|
4906
|
+
token: string;
|
|
5184
4907
|
inboxId: string;
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
policy: SendPolicy;
|
|
5192
|
-
requiredPolicy: SendPolicy;
|
|
5193
|
-
riskFlags: string[];
|
|
5194
|
-
expect: Expectation;
|
|
5195
|
-
challengeHash?: string | undefined;
|
|
5196
|
-
challengeAttempts: number;
|
|
5197
|
-
state: ApprovalState;
|
|
4908
|
+
operation: string;
|
|
4909
|
+
/** Digest of the operation parameters (labels added/removed, archive, trash…). */
|
|
4910
|
+
paramsDigest: string;
|
|
4911
|
+
/** Digest of the sorted id list. */
|
|
4912
|
+
idsDigest: string;
|
|
4913
|
+
count: number;
|
|
5198
4914
|
createdAt: string;
|
|
5199
4915
|
expiresAt: string;
|
|
5200
|
-
updatedAt: string;
|
|
5201
|
-
sentMessageId?: string | undefined;
|
|
5202
|
-
reason?: string | undefined;
|
|
5203
|
-
change?: ChangeBinding | undefined;
|
|
5204
|
-
download?: DownloadBinding | undefined;
|
|
5205
4916
|
}
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
readonly legacy: true;
|
|
5217
|
-
readonly createdAt: string;
|
|
5218
|
-
readonly expiresAt: string;
|
|
5219
|
-
/** The stored owner, trusted as stored: a version-1 record has no binding to check it against. */
|
|
5220
|
-
readonly inboxId: string;
|
|
5221
|
-
readonly ownerScope: "owner" | "global";
|
|
4917
|
+
declare class PlanStore {
|
|
4918
|
+
#private;
|
|
4919
|
+
readonly directory: string;
|
|
4920
|
+
constructor(stateDir: string, now?: () => Date);
|
|
4921
|
+
create(input: {
|
|
4922
|
+
inboxId: string;
|
|
4923
|
+
operation: string;
|
|
4924
|
+
params: unknown;
|
|
4925
|
+
ids: readonly string[];
|
|
4926
|
+
}): Promise<PlanRecord>;
|
|
5222
4927
|
/**
|
|
5223
|
-
*
|
|
5224
|
-
*
|
|
5225
|
-
* grouping and gets only generic wording.
|
|
4928
|
+
* Consumes a plan exactly once. It must be for the same inbox, operation, parameters and ids; the file is deleted
|
|
4929
|
+
* inside the lock, so a second consume fails.
|
|
5226
4930
|
*/
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
4931
|
+
consume(token: string, expected: {
|
|
4932
|
+
inboxId: string;
|
|
4933
|
+
operation: string;
|
|
4934
|
+
params: unknown;
|
|
4935
|
+
ids: readonly string[];
|
|
4936
|
+
}): Promise<PlanRecord>;
|
|
5231
4937
|
}
|
|
5232
4938
|
//#endregion
|
|
5233
|
-
//#region src/
|
|
4939
|
+
//#region src/secrets.d.ts
|
|
5234
4940
|
/**
|
|
5235
|
-
*
|
|
5236
|
-
*
|
|
4941
|
+
* Where refresh tokens, client secrets and approval keys live. Two backends, chosen per inbox or client and recorded
|
|
4942
|
+
* in config; the choice is never switched at runtime and there is no silent fallback from one to the other.
|
|
4943
|
+
*
|
|
4944
|
+
* - `keychain`: the OS store (macOS Keychain, Windows Credential Manager, Linux Secret Service — pinned, never the
|
|
4945
|
+
* kernel keyring, which forgets everything on reboot) through the optional `@napi-rs/keyring` package.
|
|
4946
|
+
* - `file`: one owner-only JSON file per secret in an owner-only directory. Chosen explicitly, never by default.
|
|
5237
4947
|
*/
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
/**
|
|
5245
|
-
|
|
5246
|
-
ids?: Record<string, string | string[] | CondensedIds>;
|
|
5247
|
-
recipientDomains?: string[];
|
|
4948
|
+
type SecretStoreKind = "keychain" | "file";
|
|
4949
|
+
interface SecretStore {
|
|
4950
|
+
readonly kind: SecretStoreKind;
|
|
4951
|
+
get(ref: string): Promise<string | null>;
|
|
4952
|
+
set(ref: string, value: string): Promise<void>;
|
|
4953
|
+
delete(ref: string): Promise<boolean>;
|
|
4954
|
+
/** Forgets any cached value, so the next read goes to the backend (e.g. after another process re-authorised). */
|
|
4955
|
+
invalidate(ref: string): void;
|
|
5248
4956
|
/**
|
|
5249
|
-
*
|
|
5250
|
-
*
|
|
5251
|
-
*
|
|
4957
|
+
* Resolves once no earlier call is still occupying the backend. Absent on a store with nothing to wait for.
|
|
4958
|
+
*
|
|
4959
|
+
* For a caller that must not give up on a write: the keychain fails every call fast while a timed-out native call
|
|
4960
|
+
* is still held by an OS dialog, so retrying on a timer spends every attempt against the same stuck call. A
|
|
4961
|
+
* refresh that has already spent Slack's single-use token is that caller — it has one value it cannot get again,
|
|
4962
|
+
* and waiting for the dialog to be answered is the only retry that can succeed.
|
|
5252
4963
|
*/
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
4964
|
+
settled?(): Promise<void>;
|
|
4965
|
+
}
|
|
4966
|
+
//#endregion
|
|
4967
|
+
//#region src/standing-ledger.d.ts
|
|
4968
|
+
/**
|
|
4969
|
+
* The acknowledgement ledger (design 2026-10-19-standing-approvals, S6, S9): one JSONL file per account,
|
|
4970
|
+
* `<state>/standing/<accountId>.jsonl`, shared by every process and locked at `<path>.lock`.
|
|
4971
|
+
*
|
|
4972
|
+
* Every use of a standing approval of the account is a `use` line, written under the lock before its one-shot child
|
|
4973
|
+
* is minted; what became of the act is an `outcome` line after it. The counts — per thread across every standing
|
|
4974
|
+
* approval of the account, per hour and per day of each one — are read from the `use` lines alone, so nothing an
|
|
4975
|
+
* outcome says frees one, and nothing ever removes a line. A count is spent when it is written, never refunded.
|
|
4976
|
+
*/
|
|
4977
|
+
/** One use: which standing approval, which child, which content, where, and what the acknowledged message said. */
|
|
4978
|
+
interface AckUse {
|
|
4979
|
+
kind: "use";
|
|
4980
|
+
at: string;
|
|
4981
|
+
standingId: string;
|
|
4982
|
+
childId: string;
|
|
4983
|
+
act: StandingAct;
|
|
4984
|
+
contentDigest: string;
|
|
4985
|
+
channel: string;
|
|
4986
|
+
ts: string;
|
|
4987
|
+
/** The thread the message is in — its parent's `ts` — or the message's own `ts` outside one. */
|
|
4988
|
+
thread: string;
|
|
4989
|
+
/** Who wrote the acknowledged message, by their Slack id: what `perSender` counts by, across the account. */
|
|
4990
|
+
sender: string;
|
|
4991
|
+
/** SHA-256 of the acknowledged message's text at the act, so an edit since shows. */
|
|
4992
|
+
targetDigest: string;
|
|
4993
|
+
edited: boolean;
|
|
4994
|
+
}
|
|
4995
|
+
/** What became of one use's act. It records; it frees nothing. */
|
|
4996
|
+
interface AckOutcome {
|
|
4997
|
+
kind: "outcome";
|
|
4998
|
+
at: string;
|
|
5260
4999
|
/**
|
|
5261
|
-
*
|
|
5262
|
-
*
|
|
5263
|
-
* the configuration, and only this line tells them apart.
|
|
5000
|
+
* When the act itself started, after the pause between the decision and the act (S9, the lead's call after review):
|
|
5001
|
+
* a use's own `at` is when it was decided.
|
|
5264
5002
|
*/
|
|
5265
|
-
|
|
5003
|
+
actAt?: string;
|
|
5004
|
+
childId: string;
|
|
5005
|
+
outcome: "sent" | "failed" | "unknown";
|
|
5006
|
+
link?: string;
|
|
5007
|
+
notice: string;
|
|
5008
|
+
}
|
|
5009
|
+
declare class AckLedger {
|
|
5010
|
+
#private;
|
|
5011
|
+
readonly directory: string;
|
|
5012
|
+
constructor(stateDir: string, now?: () => Date);
|
|
5013
|
+
/** The clock this ledger was opened with: what a use is timed by, and its counts' windows measured from. */
|
|
5014
|
+
now(): Date;
|
|
5015
|
+
/** Where an account's ledger is kept: `<state>/standing/<accountId>.jsonl`, for a person told to move it aside. */
|
|
5016
|
+
pathOf(accountId: string): string;
|
|
5017
|
+
/**
|
|
5018
|
+
* Every account's ledger on this machine that holds a line this release cannot read, with the number of each such
|
|
5019
|
+
* line, for the doctor (design 2026-10-19-standing-approvals, the lead's call after review): while one does, no
|
|
5020
|
+
* standing approval of that account can be used — what it counts cannot be known — and nothing else says so. Reads
|
|
5021
|
+
* without the lock: a look, and every line it reads was written whole.
|
|
5022
|
+
*/
|
|
5023
|
+
unreadable(): Promise<{
|
|
5024
|
+
accountId: string;
|
|
5025
|
+
path: string;
|
|
5026
|
+
lines: number[];
|
|
5027
|
+
}[]>;
|
|
5266
5028
|
/**
|
|
5267
|
-
*
|
|
5268
|
-
*
|
|
5269
|
-
*
|
|
5029
|
+
* Runs `fn` under the account's ledger lock, with every entry as it reads there and `append`, which writes a `use`
|
|
5030
|
+
* line under the lock already held. `fn` must never take this lock again.
|
|
5031
|
+
*
|
|
5032
|
+
* A line this release cannot read refuses the whole call, BAD_DATA, before `fn` runs: a count read without it could
|
|
5033
|
+
* be lower than the truth, and a count is never guessed downwards.
|
|
5270
5034
|
*/
|
|
5271
|
-
|
|
5035
|
+
withLock<T>(accountId: string, fn: (entries: (AckUse | AckOutcome)[], append: (entry: AckUse) => Promise<void>) => Promise<T>): Promise<T>;
|
|
5036
|
+
/** Records what became of a use's act, taking the lock itself. It never frees a count. */
|
|
5037
|
+
recordOutcome(accountId: string, entry: AckOutcome): Promise<void>;
|
|
5038
|
+
/** Every entry this release can read, oldest first, for a report: a line it cannot read is left out of it. */
|
|
5039
|
+
entries(accountId: string): Promise<(AckUse | AckOutcome)[]>;
|
|
5272
5040
|
}
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5041
|
+
//#endregion
|
|
5042
|
+
//#region src/state.d.ts
|
|
5043
|
+
/**
|
|
5044
|
+
* Per-inbox runtime facts that any process may update (last successful refresh, last use, health). Kept out of
|
|
5045
|
+
* config.json so a running server never rewrites user intent — a stale in-memory copy of config written back by a
|
|
5046
|
+
* server could otherwise silently undo a policy the user just tightened.
|
|
5047
|
+
*/
|
|
5048
|
+
interface InboxRuntimeState {
|
|
5049
|
+
lastRefreshOkAt?: string | undefined;
|
|
5050
|
+
lastUsedAt?: string | undefined;
|
|
5051
|
+
/** Set to `undefined` to clear it: the merged value is dropped when the file is written. */
|
|
5052
|
+
lastError?: {
|
|
5053
|
+
code: string;
|
|
5054
|
+
message: string;
|
|
5055
|
+
at: string;
|
|
5056
|
+
} | undefined;
|
|
5057
|
+
grantedScopes?: string[] | undefined;
|
|
5058
|
+
refreshTokenExpiresAt?: string | undefined;
|
|
5278
5059
|
}
|
|
5279
|
-
declare class
|
|
5060
|
+
declare class InboxStateStore {
|
|
5280
5061
|
#private;
|
|
5281
5062
|
readonly directory: string;
|
|
5282
|
-
constructor(stateDir: string
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5063
|
+
constructor(stateDir: string);
|
|
5064
|
+
get(inboxId: string): Promise<InboxRuntimeState>;
|
|
5065
|
+
/** Merges `patch` into the stored state under a per-inbox lock. */
|
|
5066
|
+
update(inboxId: string, patch: Partial<InboxRuntimeState>): Promise<InboxRuntimeState>;
|
|
5067
|
+
}
|
|
5068
|
+
/**
|
|
5069
|
+
* A platform identifier that is not an email address: a Slack user or conversation id.
|
|
5070
|
+
*
|
|
5071
|
+
* It carries its scope because, unlike an address, it is not globally unique — `U024BE7LH` names a different person
|
|
5072
|
+
* in every workspace that happens to mint that id. The address store is deliberately cross-inbox, on the reasoning
|
|
5073
|
+
* that a message read in one mailbox can ask for a send from another; that reasoning does not transfer here, and a
|
|
5074
|
+
* store that matched ids across workspaces would flag an unrelated person every time two workspaces collided.
|
|
5075
|
+
*
|
|
5076
|
+
* The id, never the display name. `@sam` is set by the account that bears it and can be changed to `@finance-bot`
|
|
5077
|
+
* between the message being read and the send being checked; the id cannot.
|
|
5078
|
+
*/
|
|
5079
|
+
interface TaintHandle {
|
|
5080
|
+
/** `slack`. Lower-cased on the way in. */
|
|
5081
|
+
platform: string;
|
|
5082
|
+
/** The workspace or team the id belongs to — a Slack team id. */
|
|
5083
|
+
scope: string;
|
|
5084
|
+
/** `U024BE7LH`, `C0123`, `D0456`. Passed in the platform's own canonical form; core does not know its shape. */
|
|
5085
|
+
id: string;
|
|
5086
|
+
}
|
|
5087
|
+
type TaintSource = "header" | "body";
|
|
5088
|
+
/** Why the content reached this process. This is separate from `TaintSource`, which remains the cap priority. */
|
|
5089
|
+
type TaintOrigin = "read" | "event";
|
|
5090
|
+
interface TaintObservation {
|
|
5091
|
+
address: string;
|
|
5092
|
+
source: TaintSource;
|
|
5093
|
+
/** Old readers did not record this, so an omitted origin remains an ordinary read. */
|
|
5094
|
+
origin?: TaintOrigin | undefined;
|
|
5095
|
+
inboxId: string;
|
|
5096
|
+
messageId?: string | undefined;
|
|
5294
5097
|
}
|
|
5295
|
-
/** The
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5098
|
+
/** The same observation for a handle. `inboxId` is the account that did the reading, as it is for an address. */
|
|
5099
|
+
interface TaintHandleObservation {
|
|
5100
|
+
handle: TaintHandle;
|
|
5101
|
+
source: TaintSource;
|
|
5102
|
+
/** Old readers did not record this, so an omitted origin remains an ordinary read. */
|
|
5103
|
+
origin?: TaintOrigin | undefined;
|
|
5104
|
+
inboxId: string;
|
|
5105
|
+
messageId?: string | undefined;
|
|
5106
|
+
}
|
|
5107
|
+
/** What is never recorded: the user's own addresses and the domains they call internal. */
|
|
5108
|
+
interface TaintExclusions {
|
|
5109
|
+
ownAddresses: readonly string[];
|
|
5110
|
+
internalDomains: readonly string[];
|
|
5111
|
+
/**
|
|
5112
|
+
* Handles that are never recorded: the account's own user id, and whoever the caller treats as internal — the
|
|
5113
|
+
* counterpart of `internalDomains`, decided per platform because "internal" means a domain for mail and
|
|
5114
|
+
* workspace membership for Slack, and core should not be the thing that knows the difference.
|
|
5115
|
+
*/
|
|
5116
|
+
ownHandles?: readonly TaintHandle[] | undefined;
|
|
5302
5117
|
}
|
|
5303
5118
|
/**
|
|
5304
|
-
*
|
|
5305
|
-
*
|
|
5306
|
-
*
|
|
5307
|
-
* deleted, `skippedBusy` the records whose lock was held elsewhere and were left. `complete` — the batch reached the end
|
|
5308
|
-
* of the directory inside both bounds; hitting 200 slots or the deadline, or losing the lock, leaves it false.
|
|
5119
|
+
* One stored aggregate, exactly as the store holds it (design 2026-10-05 §D4): the newest time anything matched it, the
|
|
5120
|
+
* strongest source ever seen — `header` is kept once seen — and every mailbox that recorded it. Three separate facts,
|
|
5121
|
+
* never one sighting: `touch` replaces the time, keeps a header and unions the mailboxes independently.
|
|
5309
5122
|
*/
|
|
5310
|
-
interface
|
|
5311
|
-
readonly
|
|
5312
|
-
readonly
|
|
5313
|
-
readonly
|
|
5314
|
-
|
|
5315
|
-
readonly
|
|
5316
|
-
readonly errors: readonly MaintenanceError[];
|
|
5317
|
-
readonly skipped?: "not-due" | "busy" | "no-records" | undefined;
|
|
5123
|
+
interface TaintAggregate {
|
|
5124
|
+
readonly at: string;
|
|
5125
|
+
readonly source: TaintSource;
|
|
5126
|
+
readonly inboxIds: readonly string[];
|
|
5127
|
+
/** Sorted, duplicate-free provenance. A base entry with no sidecar record is a legacy read. */
|
|
5128
|
+
readonly origins: readonly TaintOrigin[];
|
|
5318
5129
|
}
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
readonly providerId?: string | undefined;
|
|
5327
|
-
/** A record an earlier release wrote. */
|
|
5328
|
-
readonly legacy?: true | undefined;
|
|
5130
|
+
interface TaintCheck {
|
|
5131
|
+
address: boolean;
|
|
5132
|
+
domain: boolean;
|
|
5133
|
+
/** The exact address's aggregate, when it is in the window. */
|
|
5134
|
+
addressSeen?: TaintAggregate | undefined;
|
|
5135
|
+
/** Its domain's, when that counts — not a public mailbox provider — and is in the window. */
|
|
5136
|
+
domainSeen?: TaintAggregate | undefined;
|
|
5329
5137
|
}
|
|
5330
|
-
|
|
5138
|
+
/** Testable interruption point for the crash-safe, two-lock sidecar protocol. */
|
|
5139
|
+
interface TaintStoreHooks {
|
|
5140
|
+
afterOriginsCommit?: (() => Promise<void>) | undefined;
|
|
5141
|
+
onLockAcquired?: ((lock: "origins" | "taint" | "handles") => void) | undefined;
|
|
5142
|
+
}
|
|
5143
|
+
declare class TaintStore {
|
|
5144
|
+
#private;
|
|
5145
|
+
readonly directory: string;
|
|
5146
|
+
constructor(stateDir: string, now?: () => Date, hooks?: TaintStoreHooks);
|
|
5331
5147
|
/**
|
|
5332
|
-
*
|
|
5333
|
-
*
|
|
5148
|
+
* Records observations. Throws if it cannot — callers must fail the read rather than return content whose taint
|
|
5149
|
+
* was not recorded.
|
|
5334
5150
|
*/
|
|
5335
|
-
readonly
|
|
5151
|
+
record(observations: readonly TaintObservation[], exclusions: TaintExclusions, handleObservations?: readonly TaintHandleObservation[]): Promise<void>;
|
|
5152
|
+
/**
|
|
5153
|
+
* Whether an address, or its (non-public) domain, was seen in email content in the window — from any inbox — and the
|
|
5154
|
+
* aggregate each match is made of, for a caller to explain it. Nothing is written.
|
|
5155
|
+
*/
|
|
5156
|
+
check(address: string): Promise<TaintCheck>;
|
|
5157
|
+
/**
|
|
5158
|
+
* Whether a handle was seen in message content in the window.
|
|
5159
|
+
*
|
|
5160
|
+
* Only within its own workspace — see `TaintHandle`. There is no second answer to give, the way an address also
|
|
5161
|
+
* carries a domain: two ids sharing a workspace says nothing about either of them.
|
|
5162
|
+
*/
|
|
5163
|
+
checkHandle(handle: TaintHandle): Promise<boolean>;
|
|
5164
|
+
/** The merged provenance of a retained handle, or no origins when it is not in the taint window. */
|
|
5165
|
+
checkHandleOrigins(handle: TaintHandle): Promise<readonly TaintOrigin[]>;
|
|
5336
5166
|
}
|
|
5337
5167
|
//#endregion
|
|
5338
|
-
//#region src/
|
|
5339
|
-
/**
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
/**
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
* - `unreadable` — a file that cannot safely be read at all: shown only as a stub naming its id (from the file name)
|
|
5366
|
-
* and a fixed reason, never anything the file holds.
|
|
5367
|
-
*
|
|
5368
|
-
* Nothing is silently omitted, and the decoder writes nothing.
|
|
5369
|
-
*/
|
|
5370
|
-
/** Why a file cannot safely be read: a fixed category, never the file's own bytes. */
|
|
5371
|
-
type UnreadableReason = "invalid-json" | "truncated" | "missing-ownership" | "missing-kind" | "wrong-shape" | "file-name-mismatch";
|
|
5372
|
-
/** What may be said of a record that cannot be trusted: its id, that it is corrupt, and why — nothing else. */
|
|
5373
|
-
interface CorruptStub {
|
|
5374
|
-
readonly approvalId: string;
|
|
5375
|
-
readonly state: "corrupt";
|
|
5376
|
-
readonly reason: UnreadableReason | IntegrityReason;
|
|
5377
|
-
}
|
|
5378
|
-
/** What an attribution-verified corrupt record may still show its owner: the fields its binding was made over. */
|
|
5379
|
-
interface OrdinarySafeFields {
|
|
5380
|
-
readonly kind: ApprovalKind;
|
|
5381
|
-
readonly channel: string;
|
|
5382
|
-
readonly ownerScope: OwnerScope;
|
|
5383
|
-
readonly inboxId: string;
|
|
5384
|
-
readonly inboxSub?: string | undefined;
|
|
5385
|
-
readonly draftId: string;
|
|
5386
|
-
readonly draftMessageId: string;
|
|
5387
|
-
readonly expect: Expectation;
|
|
5388
|
-
}
|
|
5389
|
-
/** A disclosure record has no channel owner or sender-controlled operational fields to expose. */
|
|
5390
|
-
interface DisclosureSafeFields {
|
|
5391
|
-
readonly kind: "disclosure";
|
|
5168
|
+
//#region src/core.d.ts
|
|
5169
|
+
/** Everything a provider package needs from the core, wired to one config directory. */
|
|
5170
|
+
interface Core {
|
|
5171
|
+
paths: ResolvedPaths;
|
|
5172
|
+
/** The subset of `paths` explicitly pinned by global CLI options. */
|
|
5173
|
+
pathOverrides: Readonly<PathOverrides>;
|
|
5174
|
+
config: ConfigStore;
|
|
5175
|
+
states: InboxStateStore;
|
|
5176
|
+
approvals: ApprovalStore;
|
|
5177
|
+
ledger: SendLedger;
|
|
5178
|
+
/**
|
|
5179
|
+
* The acknowledgement ledger (design 2026-10-19-standing-approvals, S6, S9): every use of a standing approval,
|
|
5180
|
+
* counted per account, under its own lock, before the use's child is minted — and never refunded.
|
|
5181
|
+
*/
|
|
5182
|
+
acks: AckLedger;
|
|
5183
|
+
plans: PlanStore;
|
|
5184
|
+
taint: TaintStore;
|
|
5185
|
+
audit: AuditLog;
|
|
5186
|
+
/** Opens the config directory's one secret backend (as recorded in config, or `kind` before the first write). */
|
|
5187
|
+
secrets(kind?: SecretStoreKind): Promise<SecretStore>;
|
|
5188
|
+
/**
|
|
5189
|
+
* The commands this process tells a person to run — its own CLI's, core's, another product's — located from the
|
|
5190
|
+
* caller it was opened with (`OpenCoreOptions.caller`), for these folders. Undefined when none was given: then there
|
|
5191
|
+
* is no command to print at all, and anything that asks for one meets the programming error `requireHandoffs` throws —
|
|
5192
|
+
* never a bare name. Every package opens core with its caller.
|
|
5193
|
+
*/
|
|
5194
|
+
handoffs?: CliHandoffs | undefined;
|
|
5392
5195
|
}
|
|
5393
|
-
type SafeFields = OrdinarySafeFields | DisclosureSafeFields;
|
|
5394
|
-
type StoredApproval = {
|
|
5395
|
-
readonly form: "v2";
|
|
5396
|
-
readonly record: ApprovalRecord;
|
|
5397
|
-
} | {
|
|
5398
|
-
readonly form: "legacy";
|
|
5399
|
-
readonly view: LegacyView;
|
|
5400
|
-
readonly record: LegacyApprovalRecord;
|
|
5401
|
-
} | {
|
|
5402
|
-
readonly form: "corrupt";
|
|
5403
|
-
readonly approvalId: string;
|
|
5404
|
-
readonly reason: IntegrityReason;
|
|
5405
|
-
readonly attribution: Attribution;
|
|
5406
|
-
/** Only when attribution is verified. */
|
|
5407
|
-
readonly safe: SafeFields | null;
|
|
5408
|
-
} | {
|
|
5409
|
-
readonly form: "unreadable";
|
|
5410
|
-
readonly stub: CorruptStub;
|
|
5411
|
-
};
|
|
5412
5196
|
//#endregion
|
|
5413
|
-
//#region src/
|
|
5197
|
+
//#region src/standing.d.ts
|
|
5414
5198
|
/**
|
|
5415
|
-
*
|
|
5199
|
+
* Standing approvals (design 2026-10-19-standing-approvals): the person's yes given in advance, for acknowledging — an
|
|
5200
|
+
* exact set of contents they wrote, a bounded set of places, a bounded number of uses, and a time limit. The agent
|
|
5201
|
+
* decides whether to acknowledge a message that has just arrived, and with which of the contents; it never writes the
|
|
5202
|
+
* words, and it cannot pick an old message or a new place.
|
|
5416
5203
|
*
|
|
5417
|
-
*
|
|
5418
|
-
*
|
|
5419
|
-
*
|
|
5420
|
-
|
|
5421
|
-
*/
|
|
5422
|
-
|
|
5423
|
-
type
|
|
5424
|
-
/**
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
interface ApprovalObject {
|
|
5431
|
-
readonly id: string;
|
|
5432
|
-
readonly kind: ApprovalKind | null;
|
|
5433
|
-
readonly channel: string | null;
|
|
5434
|
-
readonly state: PublicApprovalState;
|
|
5435
|
-
readonly claimable: boolean;
|
|
5436
|
-
readonly route?: ApprovalRoute | undefined;
|
|
5437
|
-
readonly ownerRemoved?: true | undefined;
|
|
5438
|
-
/** A record from an earlier release, read by its own rules: shown, never claimed. */
|
|
5439
|
-
readonly legacy?: true | undefined;
|
|
5440
|
-
readonly reason?: string | undefined;
|
|
5441
|
-
readonly createdAt?: string | undefined;
|
|
5442
|
-
readonly expiresAt?: string | undefined;
|
|
5443
|
-
readonly approvedAt?: string | undefined;
|
|
5444
|
-
readonly usableUntil?: string | undefined;
|
|
5445
|
-
readonly sendingAt?: string | undefined;
|
|
5446
|
-
readonly sendingHeartbeatAt?: string | undefined;
|
|
5447
|
-
/** When a `sending` record reads `unknown` (`unknownAtOf`): derived, never stored. Also on an `unknown` one. */
|
|
5448
|
-
readonly unknownAt?: string | undefined;
|
|
5449
|
-
readonly usedAt?: string | undefined;
|
|
5450
|
-
readonly sentAt?: string | undefined;
|
|
5451
|
-
readonly sentMessageId?: string | undefined;
|
|
5452
|
-
readonly failedAt?: string | undefined;
|
|
5453
|
-
readonly revokedAt?: string | undefined;
|
|
5454
|
-
readonly expiredAt?: string | undefined;
|
|
5455
|
-
}
|
|
5456
|
-
/** What an action on a record is, now. */
|
|
5457
|
-
interface ApprovalOutcome {
|
|
5458
|
-
readonly state: PublicApprovalState;
|
|
5459
|
-
readonly claimable: boolean;
|
|
5460
|
-
readonly ownerRemoved?: true | undefined;
|
|
5461
|
-
readonly reason?: string | undefined;
|
|
5462
|
-
/**
|
|
5463
|
-
* A version-2 record as this classification reads it: with a revocation it derived applied (an owner removed, a
|
|
5464
|
-
* stale send epoch, or — for a claim or an approval — a live `never`). Null for every other form.
|
|
5465
|
-
*/
|
|
5466
|
-
readonly record: ApprovalRecord | null;
|
|
5467
|
-
/** Whether `record` carries a revocation the classification derived, for a locked action to persist. */
|
|
5468
|
-
readonly revokes: boolean;
|
|
5469
|
-
/** Where it stands, as D8 shows it (`publicApproval`). */
|
|
5470
|
-
readonly approval: ApprovalObject;
|
|
5471
|
-
/** What the action is refused with, when it is refused by the record's state alone. */
|
|
5472
|
-
readonly error?: CommsError | undefined;
|
|
5204
|
+
* This module is the model: the contents, each named by its digest and never by its position (S3); the places (S5);
|
|
5205
|
+
* the limits (S6); the digest a standing record is bound to; and what widens one, which only a person may approve at a
|
|
5206
|
+
* terminal (S8). Pure, but for `raiseStandingEpoch`: nothing else here reads a file or a clock.
|
|
5207
|
+
*/
|
|
5208
|
+
/** S1: the acts a standing approval may cover. A reaction is only ever added, never removed. */
|
|
5209
|
+
declare const STANDING_ACTS: readonly ["reaction", "reply"];
|
|
5210
|
+
type StandingAct = (typeof STANDING_ACTS)[number];
|
|
5211
|
+
/** One content the person wrote: a standard emoji's name, or a fixed reply. Named by `digest`, never by position. */
|
|
5212
|
+
interface StandingContent {
|
|
5213
|
+
kind: StandingAct;
|
|
5214
|
+
text: string;
|
|
5215
|
+
/** `sha256Hex(canonicalJson({ kind, text }))`: the kind and the exact text, and nothing else. */
|
|
5216
|
+
digest: string;
|
|
5473
5217
|
}
|
|
5474
5218
|
/**
|
|
5475
|
-
*
|
|
5476
|
-
*
|
|
5477
|
-
* was "accepted by Resend at …"; core's generic surfaces "accepted by <provider> at …".
|
|
5219
|
+
* Who shares a place, as recorded when it was named (S5). `externalMembers` is null when the place has 50 or more
|
|
5220
|
+
* members, which are not read one by one.
|
|
5478
5221
|
*/
|
|
5479
|
-
|
|
5222
|
+
interface SharedState {
|
|
5223
|
+
extShared: boolean;
|
|
5224
|
+
pendingExtShared: boolean;
|
|
5225
|
+
externalMembers: boolean | null;
|
|
5226
|
+
}
|
|
5227
|
+
/** A channel or DM named exactly, with what the person was shown of it: its name, its member count and its sharing. */
|
|
5228
|
+
interface ExactPlace {
|
|
5229
|
+
id: string;
|
|
5230
|
+
name: string | null;
|
|
5231
|
+
members: number;
|
|
5232
|
+
shared: SharedState;
|
|
5233
|
+
}
|
|
5234
|
+
/** S5: exact channels or DMs, or the threads in one workspace where the person posted themselves. */
|
|
5235
|
+
type StandingPlaces = {
|
|
5236
|
+
kind: "exact";
|
|
5237
|
+
places: ExactPlace[];
|
|
5238
|
+
} | {
|
|
5239
|
+
kind: "threads-i-am-in";
|
|
5240
|
+
};
|
|
5480
5241
|
/**
|
|
5481
|
-
*
|
|
5482
|
-
*
|
|
5242
|
+
* S6: the limits. `perThread` and `perSender` count across every standing approval of the account: `perSender` is how
|
|
5243
|
+
* many acts a day may answer messages from any one person (the lead's call, after review), the cap on what an injected
|
|
5244
|
+
* agent can say to one recipient by choosing which of their messages to acknowledge.
|
|
5483
5245
|
*/
|
|
5484
|
-
|
|
5246
|
+
interface StandingLimits {
|
|
5247
|
+
perThread: number;
|
|
5248
|
+
perHour: number;
|
|
5249
|
+
perDay: number;
|
|
5250
|
+
perSender: number;
|
|
5251
|
+
expiresDays: number;
|
|
5252
|
+
}
|
|
5253
|
+
/** Exactly what a standing record permits, stored on it so a terminal can show it again and prove it is what was made. */
|
|
5254
|
+
interface StandingBinding {
|
|
5255
|
+
/** One line about it, for a listing. Shown; not in the digest. */
|
|
5256
|
+
summary: string;
|
|
5257
|
+
target: {
|
|
5258
|
+
kind: "account";
|
|
5259
|
+
name: string;
|
|
5260
|
+
id: string;
|
|
5261
|
+
};
|
|
5262
|
+
/** Reactions first, then replies, as written. */
|
|
5263
|
+
contents: StandingContent[];
|
|
5264
|
+
places: StandingPlaces;
|
|
5265
|
+
limits: StandingLimits;
|
|
5266
|
+
/** Who proposed it: an agent, over MCP or without a person at the terminal, or the person at their terminal. */
|
|
5267
|
+
proposedBy: "agent" | "person";
|
|
5268
|
+
/** The standing id this widens, which approving it ends. */
|
|
5269
|
+
replaces?: string | undefined;
|
|
5270
|
+
/** S3's warnings, as the preview shows them. */
|
|
5271
|
+
warnings: string[];
|
|
5272
|
+
}
|
|
5273
|
+
/** What a narrowing keeps: content digests, exact place ids, and lower limits. It applies at once. */
|
|
5274
|
+
interface StandingNarrowing {
|
|
5275
|
+
keep?: string[];
|
|
5276
|
+
places?: string[];
|
|
5277
|
+
limits?: Partial<StandingLimits>;
|
|
5278
|
+
}
|
|
5485
5279
|
//#endregion
|
|
5486
5280
|
//#region src/approvals.d.ts
|
|
5487
5281
|
/**
|
|
@@ -5516,7 +5310,7 @@ type ApprovalChannel = "elicitation" | "terminal";
|
|
|
5516
5310
|
* and a kind on every record, so that none can be spent as another. Without it, a person who approved a post at a
|
|
5517
5311
|
* terminal would also have approved whatever change an agent claimed under the same id.
|
|
5518
5312
|
*/
|
|
5519
|
-
type ApprovalKind = "send" | "change" | "download" | "disclosure";
|
|
5313
|
+
type ApprovalKind = "send" | "change" | "download" | "disclosure" | "standing";
|
|
5520
5314
|
/** The inbox or account a change is about. `id` is absent when the change connects it, and it does not exist yet. */
|
|
5521
5315
|
interface ChangeTarget {
|
|
5522
5316
|
kind: "inbox" | "account";
|
|
@@ -5656,8 +5450,11 @@ interface ApprovalRecord {
|
|
|
5656
5450
|
* terminal or in a trusted form. Never on a send or a change, which carry `approvedBindingDigest` instead.
|
|
5657
5451
|
*/
|
|
5658
5452
|
approvedDigest?: string | undefined;
|
|
5659
|
-
/**
|
|
5660
|
-
|
|
5453
|
+
/**
|
|
5454
|
+
* Where a person approved, or answered: the terminal, or a trusted client's form. Absent on a chat-route claim.
|
|
5455
|
+
* `standing` on a standing approval's child, and on nothing else: the person approved it in advance, at a terminal.
|
|
5456
|
+
*/
|
|
5457
|
+
approvedVia?: ApprovalChannel | "standing" | undefined;
|
|
5661
5458
|
/** A send's or a change's approval: when, and the binding the person approved, which must still be the record's. */
|
|
5662
5459
|
approvedAt?: string | undefined;
|
|
5663
5460
|
/** `approvedAt + approvedMs`: until when an approved send or change may be claimed. */
|
|
@@ -5695,6 +5492,29 @@ interface ApprovalRecord {
|
|
|
5695
5492
|
download?: DownloadBinding | undefined;
|
|
5696
5493
|
/** For a standing disclosure: the canonical, content-free event activation authority. */
|
|
5697
5494
|
disclosure?: DisclosureBinding | undefined;
|
|
5495
|
+
/**
|
|
5496
|
+
* For a standing approval (`kind: 'standing'`, design 2026-10-19-standing-approvals): exactly what it permits.
|
|
5497
|
+
* `contentDigest` is `standingDigest` of this.
|
|
5498
|
+
*/
|
|
5499
|
+
standing?: StandingBinding | undefined;
|
|
5500
|
+
/** For a standing approval: what has been narrowed since, at once. Not bound: it can only take away (S8). */
|
|
5501
|
+
narrowed?: StandingNarrowing | undefined;
|
|
5502
|
+
/** On a standing approval and its children: the standing epoch as read when it was made. Bound. */
|
|
5503
|
+
standingEpoch?: number | undefined;
|
|
5504
|
+
/** On a standing approval's child: the standing approval it is one use of. Bound. */
|
|
5505
|
+
standingId?: string | undefined;
|
|
5506
|
+
/**
|
|
5507
|
+
* On a standing approval's child: where its act goes and which of its standing approval's contents it sends, by
|
|
5508
|
+
* digest — what its claim checks the standing approval still covers. Bound.
|
|
5509
|
+
*/
|
|
5510
|
+
standingUse?: StandingUseBinding | undefined;
|
|
5511
|
+
/**
|
|
5512
|
+
* On an approved standing approval: the change policy that governed its account when the person approved it — its
|
|
5513
|
+
* own, else the default. Written with the approval, beside `approvedAt`. A live change policy stricter than this has
|
|
5514
|
+
* ended it (`changePolicy-tightened`), whatever the standing epoch says: a release that knows no standing epoch can
|
|
5515
|
+
* still tighten the policy (design 2026-10-19-standing-approvals, S8, the lead's call after review).
|
|
5516
|
+
*/
|
|
5517
|
+
approvedChangePolicy?: ChangePolicy | undefined;
|
|
5698
5518
|
}
|
|
5699
5519
|
/**
|
|
5700
5520
|
* The stored branch for standing disclosure. Its runtime decoder refuses every field belonging to send, change or
|
|
@@ -5834,511 +5654,976 @@ interface ApprovalStoreOptions {
|
|
|
5834
5654
|
* The file operations daily maintenance and the unsent report go through — the real ones when left out; a test's to
|
|
5835
5655
|
* count and interrupt every step. Every other read and write of the store is unaffected.
|
|
5836
5656
|
*/
|
|
5837
|
-
io?: ApprovalIo | undefined;
|
|
5657
|
+
io?: ApprovalIo | undefined;
|
|
5658
|
+
/**
|
|
5659
|
+
* Told when a person has answered a send or post: approved at a terminal, or claimed to be sent — the person's yes
|
|
5660
|
+
* under the chat policy (design 2026-10-16, U7: the security line a preview carried counts as asked once its approval
|
|
5661
|
+
* is answered). Never when it is only prepared, refused or revoked. Awaited, and whatever it throws is ignored:
|
|
5662
|
+
* nothing it does may stop a send. `openCore` passes `securityAnswered`.
|
|
5663
|
+
*/
|
|
5664
|
+
answered?: ((approvalId: string) => Promise<void>) | undefined;
|
|
5665
|
+
}
|
|
5666
|
+
declare class ApprovalStore {
|
|
5667
|
+
#private;
|
|
5668
|
+
readonly directory: string;
|
|
5669
|
+
/** `ApprovalStoreOptions.answered`, never throwing. */
|
|
5670
|
+
readonly answered: ((approvalId: string) => Promise<void>) | undefined;
|
|
5671
|
+
constructor(stateDir: string, options?: ApprovalStoreOptions);
|
|
5672
|
+
/**
|
|
5673
|
+
* Daily retention (design 2026-10-05 §D9): once a day per state directory, a bounded batch deletes valid finished
|
|
5674
|
+
* records ninety days past their finish. Awaited before every creation, list, status and wait — with its own
|
|
5675
|
+
* five-second budget — and before a report, on the report's shared deadline. Never a gate: whatever it finds, it
|
|
5676
|
+
* returns its status and the caller goes on.
|
|
5677
|
+
*/
|
|
5678
|
+
ensurePruned(options?: MaintenanceOptions): Promise<MaintenanceStatus>;
|
|
5679
|
+
create(input: CreateApprovalInput): Promise<ApprovalRecord>;
|
|
5680
|
+
/** Creates only a pending standing disclosure approval from a daemon-derived canonical binding. */
|
|
5681
|
+
createDisclosure(binding: DisclosureBinding): Promise<DisclosureApprovalRecord>;
|
|
5682
|
+
/**
|
|
5683
|
+
* One approval, in whichever of its four forms it is (`StoredApproval`), read against one configuration snapshot —
|
|
5684
|
+
* or null when there is none. Nothing is written.
|
|
5685
|
+
*/
|
|
5686
|
+
get(approvalId: string): Promise<StoredApproval | null>;
|
|
5687
|
+
/**
|
|
5688
|
+
* A look at one approval, under its lock: the record as it stands and its classification for `expect` — the same
|
|
5689
|
+
* `NOT_FOUND` as every action for an id that is not what the caller expects, before anything of the record is
|
|
5690
|
+
* classified. It writes only what reading derives (an expiry, a dead send's `unknown`), never a revocation: an owner
|
|
5691
|
+
* removed or a stale epoch is shown as revoked here, and written by the next action.
|
|
5692
|
+
*
|
|
5693
|
+
* `action` is for a surface about to act on what it reads — a terminal about to show a preview, an execute about to
|
|
5694
|
+
* read its draft — classified for that action (`approve`, `claim`, `revoke`), so the refusal is the one the action
|
|
5695
|
+
* would get. A revocation the classification derives for it is written then, as the action itself would write it:
|
|
5696
|
+
* the first locked action persists it, and this is that action's first step (design 2026-10-05 §D2).
|
|
5697
|
+
*/
|
|
5698
|
+
inspect(approvalId: string, expect?: ApprovalExpectation, options?: {
|
|
5699
|
+
action?: OutcomeAction | undefined;
|
|
5700
|
+
usedSaid?: UsedSaid | undefined;
|
|
5701
|
+
}): Promise<{
|
|
5702
|
+
stored: StoredApproval;
|
|
5703
|
+
outcome: ApprovalOutcome;
|
|
5704
|
+
}>;
|
|
5705
|
+
/**
|
|
5706
|
+
* D8's object for a record a call has just written, claimed or finished — what its result reports. Classified as a
|
|
5707
|
+
* look at it would be, against a fresh read of the configuration, without the lock and writing nothing: what decided
|
|
5708
|
+
* was the transition that wrote it. A configuration that cannot be read leaves only what the record says (nothing
|
|
5709
|
+
* claimable): a report never fails the call it reports on, least of all one that has already sent.
|
|
5710
|
+
*/
|
|
5711
|
+
approvalOf(record: ApprovalRecord): Promise<ApprovalObject>;
|
|
5712
|
+
/**
|
|
5713
|
+
* The classification of a record a call has just written, in any of its forms — a revoke's, a cancel's — for its
|
|
5714
|
+
* result to show through `publicApproval`, as a status, a wait and a list show theirs: what a sender wrote only inside
|
|
5715
|
+
* the untrusted-content envelope (design 2026-10-05 §D8). Classified as a look at it would be, against a fresh read of
|
|
5716
|
+
* the configuration, without the lock and writing nothing, as `approvalOf` is; a configuration that cannot be read
|
|
5717
|
+
* leaves nothing claimable, and never fails the call it reports on.
|
|
5718
|
+
*/
|
|
5719
|
+
outcomeOf(stored: StoredApproval): Promise<ApprovalOutcome>;
|
|
5720
|
+
/** Issues a new challenge to show a human; only its hash is kept. */
|
|
5721
|
+
issueChallenge(approvalId: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
|
|
5722
|
+
usedSaid?: UsedSaid | undefined;
|
|
5723
|
+
}): Promise<string>;
|
|
5724
|
+
/** Issues the typed challenge that a terminal or the trusted desktop app must answer for disclosure. */
|
|
5725
|
+
issueDisclosureChallenge(approvalId: string): Promise<string>;
|
|
5726
|
+
/**
|
|
5727
|
+
* Approves one exact standing disclosure authority. MCP/chat forms are never an approval channel here: `app` means
|
|
5728
|
+
* the desktop application's trusted typed-challenge bridge, not a client form.
|
|
5729
|
+
*/
|
|
5730
|
+
approveDisclosure(approvalId: string, liveBinding: DisclosureBinding, answer: string, via: "terminal" | "app"): Promise<DisclosureApprovalRecord>;
|
|
5731
|
+
/**
|
|
5732
|
+
* Claims an approved disclosure once, persisting the exact `usedAt` that recovery must subsequently reuse.
|
|
5733
|
+
* A specialised caller may impose a shorter deadline; it is evaluated while this record's transition lock is held,
|
|
5734
|
+
* so a check made before waiting on that lock cannot accidentally consume the approval after it has expired.
|
|
5735
|
+
*/
|
|
5736
|
+
claimForDisclosure(approvalId: string, liveBinding: DisclosureBinding, options?: {
|
|
5737
|
+
readonly deadlineMs?: number | undefined;
|
|
5738
|
+
}): Promise<DisclosureApprovalRecord>;
|
|
5739
|
+
/**
|
|
5740
|
+
* A human approved through a confirm channel by typing the issued challenge. The draft must still be exactly what the
|
|
5741
|
+
* record was prepared for; otherwise the record is voided, because the human would be approving content the record
|
|
5742
|
+
* does not describe. Three wrong answers void it too.
|
|
5743
|
+
*
|
|
5744
|
+
* A change is approved the same way, with `kind: 'change'` and its digest standing in for the draft (see
|
|
5745
|
+
* `createChange`), so a person's typed code means one thing whichever kind of approval it is typed for.
|
|
5746
|
+
*/
|
|
5747
|
+
approve(approvalId: string, via: ApprovalChannel, live: LiveDraft, answer: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
|
|
5748
|
+
usedSaid?: UsedSaid | undefined;
|
|
5749
|
+
}): Promise<ApprovalRecord>;
|
|
5750
|
+
/**
|
|
5751
|
+
* Claims the record for sending, once. Non-consuming refusals (not yet approved) leave the record untouched so the
|
|
5752
|
+
* human can still approve it; integrity failures (another account, an edited or changed draft, different recipients
|
|
5753
|
+
* or subject) void it. Success creates the O_EXCL claim marker.
|
|
5754
|
+
*
|
|
5755
|
+
* `live.inboxId` is the owner the caller claims for: a record of another owner is the one `NOT_FOUND`, and left as it
|
|
5756
|
+
* is. The policy is the live gate's, from the configuration read under the record's lock (decision 4): a value the
|
|
5757
|
+
* caller read before the lock is exactly what a `never` change can land between.
|
|
5758
|
+
*/
|
|
5759
|
+
claimForSend(approvalId: string, live: LiveDraft & {
|
|
5760
|
+
inboxId: string;
|
|
5761
|
+
inboxSub?: string | undefined;
|
|
5762
|
+
expect: Expectation;
|
|
5763
|
+
}, options?: ClaimOptions): Promise<SendClaim>;
|
|
5764
|
+
/**
|
|
5765
|
+
* Renews the sending lease of a send this call claimed: writes `sendingHeartbeatAt` while the claim still owns a
|
|
5766
|
+
* `sending` record, and nothing otherwise. `renewed` while it is sending; `lost` once it reads `unknown` (or anything
|
|
5767
|
+
* else) — a renewal never moves a record back to `sending`. Refused, writing nothing, without the claim's token.
|
|
5768
|
+
*/
|
|
5769
|
+
heartbeat(approvalId: string, claimToken: string): Promise<"renewed" | "lost">;
|
|
5770
|
+
/**
|
|
5771
|
+
* The check before each provider step (design 2026-10-05 §D1): `go`, with the lease renewed, while this claim still
|
|
5772
|
+
* owns a `sending` record; `stop` once it reads `unknown` — another caller has decided its outcome cannot be known,
|
|
5773
|
+
* and no further step may start. Refused, writing nothing, without the claim's token.
|
|
5774
|
+
*/
|
|
5775
|
+
fence(approvalId: string, claimToken: string): Promise<"go" | "stop">;
|
|
5776
|
+
/**
|
|
5777
|
+
* A change approval, pending, bound to `changeDigest(input.change)`.
|
|
5778
|
+
*
|
|
5779
|
+
* The digest is computed here, never taken from the caller, so a record cannot claim to be bound to one change while
|
|
5780
|
+
* describing another. It stands in for the draft revision too, as a reaction's does: a change has no draft, and the
|
|
5781
|
+
* same value means the same change.
|
|
5782
|
+
*/
|
|
5783
|
+
createChange(input: CreateChangeApprovalInput): Promise<ApprovalRecord>;
|
|
5784
|
+
/**
|
|
5785
|
+
* Claims a change approval, once, for the change the caller is about to write.
|
|
5786
|
+
*
|
|
5787
|
+
* `live.change` is the change as the caller computes it now, and it has to digest to what was prepared: a different
|
|
5788
|
+
* path, a different value, a different account or a different effect voids the approval, because the person agreed
|
|
5789
|
+
* to something else. `live.policy` is the change policy in force now, and the stricter of it and the one at prepare
|
|
5790
|
+
* decides — so tightening the policy after an agent prepared a change takes effect on that change, and loosening it
|
|
5791
|
+
* does not release one prepared under `confirm`.
|
|
5792
|
+
*
|
|
5793
|
+
* Under `chat` a pending approval is claimable: the yes was given in the conversation. Anything stricter needs a
|
|
5794
|
+
* person to have typed the code at a terminal first; until then the refusal leaves the record as it is.
|
|
5795
|
+
*/
|
|
5796
|
+
claimForChange(approvalId: string, live: LiveChange, options?: ClaimOptions): Promise<ApprovalRecord>;
|
|
5797
|
+
/**
|
|
5798
|
+
* A standing approval (design 2026-10-19-standing-approvals, S8), pending, bound to `standingDigest` of its binding —
|
|
5799
|
+
* computed here, never taken from the caller — and to the send epoch of its account and the standing epoch, both as
|
|
5800
|
+
* read when it is made.
|
|
5801
|
+
*
|
|
5802
|
+
* Its route is `confirm` and its `requiredPolicy` `confirm` whatever the change policy is, because what it permits
|
|
5803
|
+
* happens with nobody asked: it is approved only with the code typed at a terminal (`approve`, `via: 'terminal'`),
|
|
5804
|
+
* never by a yes in the chat, a client's form, or an answer carried in a call. It lasts its days once approved
|
|
5805
|
+
* (`approvedMs`). Nothing ever claims it: each use mints a child (`createStandingChild`).
|
|
5806
|
+
*
|
|
5807
|
+
* The binding is checked before anything is written (`canonicalStandingBinding`): a custom emoji, a reply with a
|
|
5808
|
+
* link, a limit outside the table or a place that is no Slack id is refused, however the caller assembled it.
|
|
5809
|
+
*/
|
|
5810
|
+
createStanding(input: {
|
|
5811
|
+
binding: StandingBinding;
|
|
5812
|
+
inboxSub: string;
|
|
5813
|
+
sendEpoch: number;
|
|
5814
|
+
standingEpoch: number;
|
|
5815
|
+
}): Promise<ApprovalRecord>;
|
|
5816
|
+
/**
|
|
5817
|
+
* Runs `use` under the standing record's own lock, with the record as it reads there and its classification for a
|
|
5818
|
+
* use (`claim`): its expiry, a revocation the configuration derives — its account removed, `never`, a send epoch or
|
|
5819
|
+
* a standing epoch behind the live one — written before `use` runs, and `outcome.error` set for every state that
|
|
5820
|
+
* refuses a use. `owner` is the account the caller acts for: any other's record is the one `NOT_FOUND`.
|
|
5821
|
+
*
|
|
5822
|
+
* What `use` decides is decided while no revocation, narrowing or approval of this record can land. `use` must not
|
|
5823
|
+
* take this record's lock again: it is held.
|
|
5824
|
+
*/
|
|
5825
|
+
withStanding<T>(standingId: string, owner: string, use: (record: ApprovalRecord, outcome: ApprovalOutcome) => Promise<T>): Promise<T>;
|
|
5826
|
+
/**
|
|
5827
|
+
* One use of a standing approval (S9): a send record, approved the moment it is made — `route: 'standing'`,
|
|
5828
|
+
* `approvedVia: 'standing'`, `approvedAt` its creation — bound to the standing id, the standing epoch and the send
|
|
5829
|
+
* epoch, and usable once, within `APPROVAL_LIFETIMES.standingChild`. It then takes the one send path every send
|
|
5830
|
+
* takes: `claimForSend`, its O_EXCL marker, the fence and the outcome.
|
|
5831
|
+
*
|
|
5832
|
+
* Minted only by the use (`mintStandingUse`), under the account's acknowledgement ledger lock and then the standing
|
|
5833
|
+
* record's, after every check there. Checked again here, read without that lock — which the caller holds: the
|
|
5834
|
+
* standing record must be this account's, approved at a terminal, under the same epochs and the same account user,
|
|
5835
|
+
* and not ended, or nothing is written. A child never names anything but what its use was checked for.
|
|
5836
|
+
*/
|
|
5837
|
+
createStandingChild(input: {
|
|
5838
|
+
/**
|
|
5839
|
+
* The id the child is made under, when its use was counted under it first (`mintStandingUse`): an id this store
|
|
5840
|
+
* would make, and one no record holds. A new one when left out.
|
|
5841
|
+
*/
|
|
5842
|
+
approvalId?: string | undefined;
|
|
5843
|
+
standingId: string;
|
|
5844
|
+
standingEpoch: number;
|
|
5845
|
+
inboxId: string;
|
|
5846
|
+
inboxSub: string;
|
|
5847
|
+
draftId: string;
|
|
5848
|
+
draftMessageId: string;
|
|
5849
|
+
contentDigest: string;
|
|
5850
|
+
sendEpoch: number;
|
|
5851
|
+
riskFlags: string[];
|
|
5852
|
+
expect: Expectation;
|
|
5853
|
+
/** Where the act goes, and which of the standing approval's contents it sends: what its claim checks again. */
|
|
5854
|
+
use: StandingUseBinding;
|
|
5855
|
+
}): Promise<ApprovalRecord>;
|
|
5856
|
+
/**
|
|
5857
|
+
* Narrows a standing approval at once (S8): fewer contents or places, a lower limit, an earlier end. Nothing is asked
|
|
5858
|
+
* for, because nothing more is permitted: anything `classifyStandingChange` calls a widening of what is in force is
|
|
5859
|
+
* refused with USAGE, and the record is left as it was. The binding the person approved stays the record's own, and
|
|
5860
|
+
* what is narrowed is kept beside it, where it can only take away.
|
|
5861
|
+
*/
|
|
5862
|
+
narrowStanding(standingId: string, owner: string, narrowing: StandingNarrowing): Promise<ApprovalRecord>;
|
|
5863
|
+
/**
|
|
5864
|
+
* A download's question, pending, bound to `downloadDigest(input.download)` — computed here, never taken from the
|
|
5865
|
+
* caller, as a change's digest is. It stands in for the draft revision too.
|
|
5866
|
+
*
|
|
5867
|
+
* `policy` is the change policy of the mailbox or workspace the files come from, as it stands when the question is
|
|
5868
|
+
* asked: where a stranger's files land on this machine is a change to it, and is answered the way that account's
|
|
5869
|
+
* other changes are approved. A claim holds the question to the stricter of this and the policy then.
|
|
5870
|
+
*/
|
|
5871
|
+
createDownload(input: {
|
|
5872
|
+
/** The manifest channel the files come from. */
|
|
5873
|
+
channel: string;
|
|
5874
|
+
download: DownloadBinding;
|
|
5875
|
+
policy: ChangePolicy;
|
|
5876
|
+
}): Promise<ApprovalRecord & {
|
|
5877
|
+
download: DownloadBinding;
|
|
5878
|
+
}>;
|
|
5879
|
+
/**
|
|
5880
|
+
* Records the person's answer to a download's question, given where an agent cannot give it: at their own terminal,
|
|
5881
|
+
* or in a form a trusted client showed them. The question moves to `approved`, carrying the answer, and waits for
|
|
5882
|
+
* the download to claim it — which it may then do under `confirm`, and which saves where this answer says.
|
|
5883
|
+
*
|
|
5884
|
+
* Only a pending question can be answered, and only once: a second answer is refused, not taken over the first.
|
|
5885
|
+
*/
|
|
5886
|
+
answerDownload(approvalId: string, via: ApprovalChannel, answer: RecordedSaveAnswer, _platform?: NodeJS.Platform): Promise<ApprovalRecord & {
|
|
5887
|
+
download: DownloadBinding;
|
|
5888
|
+
}>;
|
|
5889
|
+
/**
|
|
5890
|
+
* Claims a download's question, once, for the download the caller is about to make — and returns it, with the
|
|
5891
|
+
* folders it offered, so that `downloads` and `current` in the answer mean the paths the person read, and with the
|
|
5892
|
+
* answer the person recorded, when they recorded one.
|
|
5893
|
+
*
|
|
5894
|
+
* `live` is the download as the caller computes it now: the same account, the same request, the same files under
|
|
5895
|
+
* the same names — the person answered for those files and no others. Any other is refused, and the question left
|
|
5896
|
+
* open, as it was: a second call that got an argument wrong is the agent's slip, not the person's, and voiding the
|
|
5897
|
+
* question for it would make the person answer again for nothing. It still expires, and is claimed once.
|
|
5898
|
+
*
|
|
5899
|
+
* Whether it can be claimed is the download `claimable` matrix (`downloadClaimable`, design 2026-10-05 §D2), decided
|
|
5900
|
+
* under the question's lock against the live change policy read there — never a policy the caller read before it.
|
|
5901
|
+
* While both the policy the question was asked under and the live one are `chat`, a pending question is claimed
|
|
5902
|
+
* with the answer the caller carries: the person gave it in the conversation, and it is never kept. Otherwise only a
|
|
5903
|
+
* question the person answered at a terminal or in a trusted form can be claimed; a pending one is refused, and left
|
|
5904
|
+
* as it is, so they can still answer it. One used, voided, expired or corrupt is refused for what it is.
|
|
5905
|
+
*
|
|
5906
|
+
* `options.signal` is the download's cancellation, asked under the lock as a send's is (`ClaimOptions.signal`): a
|
|
5907
|
+
* download cancelled while this waited saves nothing and leaves the question open, the answer still unused.
|
|
5908
|
+
*/
|
|
5909
|
+
claimForDownload(approvalId: string, live: DownloadRequest, options?: {
|
|
5910
|
+
pendingHint?: string | undefined;
|
|
5911
|
+
signal?: AbortSignal | undefined;
|
|
5912
|
+
platform?: NodeJS.Platform | undefined;
|
|
5913
|
+
handoffs?: CliHandoffs | undefined;
|
|
5914
|
+
}): Promise<ApprovalRecord & {
|
|
5915
|
+
download: DownloadBinding;
|
|
5916
|
+
}>;
|
|
5917
|
+
/**
|
|
5918
|
+
* Records the outcome of the one send attempt: `used` with the provider's id for it, when the provider accepted it,
|
|
5919
|
+
* or `failed`.
|
|
5920
|
+
*
|
|
5921
|
+
* `used` needs a non-empty id. A provider that accepted a send without one has not given anything to record it by,
|
|
5922
|
+
* so the record is left as it is — `sending`, and then `unknown` — rather than made `used` with an empty id that
|
|
5923
|
+
* every later reader would take for one.
|
|
5924
|
+
*
|
|
5925
|
+
* Only the claimant records it, by its claim token, from `sending` or from `unknown`: a provider's answer that
|
|
5926
|
+
* arrives after the lease ran out is still the truth. A late success clears the stale-lease reason; a late failure
|
|
5927
|
+
* replaces it with its own.
|
|
5928
|
+
*/
|
|
5929
|
+
complete(approvalId: string, claimToken: string, outcome: SendOutcome): Promise<ApprovalRecord>;
|
|
5930
|
+
/**
|
|
5931
|
+
* Voids a pending or approved record. Other records are left as they are, and returned as they read.
|
|
5932
|
+
*
|
|
5933
|
+
* `disposition` says whose decision this is, and has no default: every caller chooses (`RevokeDisposition`). A
|
|
5934
|
+
* version-1 record is retired in its own shape when a person or an owner's removal revokes it (`revokeLegacy`), and
|
|
5935
|
+
* refused with the version refusal, nothing written, when this release's integrity check would have voided it. A
|
|
5936
|
+
* corrupt or unreadable record is never revoked or rewritten: it is refused with the integrity refusal, which repeats
|
|
5937
|
+
* only its stub.
|
|
5938
|
+
*/
|
|
5939
|
+
revoke(approvalId: string, reason: string, options: {
|
|
5940
|
+
disposition: RevokeDisposition;
|
|
5941
|
+
expect?: ApprovalExpectation | undefined;
|
|
5942
|
+
}): Promise<StoredApproval>;
|
|
5838
5943
|
/**
|
|
5839
|
-
*
|
|
5840
|
-
*
|
|
5841
|
-
*
|
|
5842
|
-
*
|
|
5944
|
+
* Retires a record from an earlier release (version 1) in that release's own shape, so a process still running it
|
|
5945
|
+
* can no longer claim the record — and nothing more.
|
|
5946
|
+
*
|
|
5947
|
+
* Under the record's own lock, `<id>.json.lock`, the one 0.13.0's transitions take, so the two serialise. The file
|
|
5948
|
+
* is read again there, decoded, and its state derived by its own release's rules (`deriveLegacyV1State`). Only a
|
|
5949
|
+
* `pending` or `approved` record is rewritten, as itself plus `state: 'revoked'`, the reason and `updatedAt`: its
|
|
5950
|
+
* digest version stays 1, a send stays without a `kind`, and no field of this release's is added — no claim marker
|
|
5951
|
+
* either. Any other derived state is returned as it reads and written nothing: an expired record is never rewritten,
|
|
5952
|
+
* as `revoke` leaves every finished record as it is.
|
|
5953
|
+
*
|
|
5954
|
+
* It reads no configuration and needs no owner, so it works on a record whose account is already gone. Its only
|
|
5955
|
+
* callers are a person's revoke, the removal of the record's owner, and the legacy drain.
|
|
5843
5956
|
*/
|
|
5844
|
-
|
|
5845
|
-
}
|
|
5846
|
-
declare class ApprovalStore {
|
|
5847
|
-
#private;
|
|
5848
|
-
readonly directory: string;
|
|
5849
|
-
/** `ApprovalStoreOptions.answered`, never throwing. */
|
|
5850
|
-
readonly answered: ((approvalId: string) => Promise<void>) | undefined;
|
|
5851
|
-
constructor(stateDir: string, options?: ApprovalStoreOptions);
|
|
5957
|
+
revokeLegacy(approvalId: string, reason: string): Promise<LegacyApprovalRecord>;
|
|
5852
5958
|
/**
|
|
5853
|
-
*
|
|
5854
|
-
*
|
|
5855
|
-
*
|
|
5856
|
-
*
|
|
5959
|
+
* Every approval file, each looked at under its own lock and classified there, as `inspect` looks at one — against one
|
|
5960
|
+
* configuration snapshot, loaded once before any file is read (design 2026-10-05 §D2, §D8). What a list shows: each
|
|
5961
|
+
* record's form and its outcome, nothing skipped. Like a look, it writes only what reading derives — an expiry at its
|
|
5962
|
+
* boundary, a stale send's `unknown` — never a revocation: an owner removed or a stale epoch is shown as revoked, and
|
|
5963
|
+
* written by the next action.
|
|
5964
|
+
*
|
|
5965
|
+
* `inboxId` keeps the records whose owner is that id (`ownerOf`), so one whose owner cannot be trusted never matches;
|
|
5966
|
+
* `states` keeps those whose classified state is listed — a stored state, `corrupt`, and for a download's answered
|
|
5967
|
+
* question its stored `approved` or `used`.
|
|
5857
5968
|
*/
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5969
|
+
inspectAll(filter?: {
|
|
5970
|
+
inboxId?: string;
|
|
5971
|
+
states?: (ApprovalState | "corrupt")[];
|
|
5972
|
+
}): Promise<Array<{
|
|
5973
|
+
stored: StoredApproval;
|
|
5974
|
+
outcome: ApprovalOutcome;
|
|
5975
|
+
}>>;
|
|
5862
5976
|
/**
|
|
5863
|
-
*
|
|
5864
|
-
*
|
|
5977
|
+
* Every approval file, each in one of its four forms — nothing skipped, nothing omitted — read against one
|
|
5978
|
+
* configuration snapshot, loaded once before any file is read: a loader that fails fails the whole list.
|
|
5979
|
+
*
|
|
5980
|
+
* `inboxId` keeps the records whose owner is that id (`ownerOf`), so a file whose owner cannot be trusted never
|
|
5981
|
+
* matches one; `states` keeps those whose state (`stateOf`, `corrupt` for one that cannot be used) is listed.
|
|
5865
5982
|
*/
|
|
5866
|
-
|
|
5983
|
+
list(filter?: {
|
|
5984
|
+
inboxId?: string;
|
|
5985
|
+
states?: (ApprovalState | "corrupt")[];
|
|
5986
|
+
}): Promise<StoredApproval[]>;
|
|
5987
|
+
}
|
|
5988
|
+
//#endregion
|
|
5989
|
+
//#region src/config-hooks.d.ts
|
|
5990
|
+
/**
|
|
5991
|
+
* Test-only pauses inside `ConfigStore.convertToVersion3`, for the tests that prove a write racing the conversion is
|
|
5992
|
+
* neither lost nor let in between its scan and its write.
|
|
5993
|
+
*
|
|
5994
|
+
* Deliberately not exported from the package root, as `enableNamesMigrationForTests` is not (`release-gate.ts`): a
|
|
5995
|
+
* store is given them through its constructor under this symbol, which only a test importing this file by path can
|
|
5996
|
+
* name. No consumer of the package can reach it.
|
|
5997
|
+
*/
|
|
5998
|
+
declare const CONVERSION_HOOKS: unique symbol;
|
|
5999
|
+
interface ConversionHooks {
|
|
6000
|
+
/** After the read before the lock, before the config lock is taken. */
|
|
6001
|
+
beforeLock?: (() => Promise<void> | void) | undefined;
|
|
6002
|
+
/** Under the lock, after the approvals were scanned and before the version-3 write is built. */
|
|
6003
|
+
afterScan?: (() => Promise<void> | void) | undefined;
|
|
6004
|
+
/** Under the lock, immediately before the version-3 write. */
|
|
6005
|
+
beforeWrite?: (() => Promise<void> | void) | undefined;
|
|
6006
|
+
}
|
|
6007
|
+
type SendPolicy = "chat" | "confirm" | "never";
|
|
6008
|
+
/**
|
|
6009
|
+
* How a loosening of the configuration is approved: `chat` — the person says yes in the conversation and the agent
|
|
6010
|
+
* claims the approval — or `confirm` — the person types a code at a terminal, as for a send under `confirm`.
|
|
6011
|
+
*
|
|
6012
|
+
* There is no `never`. A setting the software refused ever to loosen would leave editing the file by hand as the only
|
|
6013
|
+
* way to change it, and that passes no gate at all.
|
|
6014
|
+
*/
|
|
6015
|
+
type ChangePolicy = "chat" | "confirm";
|
|
6016
|
+
/** 0.18's switch for the daily update check: what `defaults.updates` reads from until a level is set. */
|
|
6017
|
+
type UpdateCheckSetting = "on" | "off";
|
|
6018
|
+
type StoreKind = "keychain" | "file";
|
|
6019
|
+
interface ClientConfig {
|
|
6020
|
+
provider: string;
|
|
6021
|
+
clientId: string;
|
|
6022
|
+
projectId?: string | undefined;
|
|
6023
|
+
secretRef: string;
|
|
6024
|
+
addedAt: string;
|
|
5867
6025
|
/**
|
|
5868
|
-
*
|
|
5869
|
-
*
|
|
5870
|
-
*
|
|
5871
|
-
* removed or a stale epoch is shown as revoked here, and written by the next action.
|
|
6026
|
+
* The organisation profile that made this row and owns it (design 2026-10-02 §D4) — absent on a row a person
|
|
6027
|
+
* registered, or one a profile adopted. A row carrying it is the profile's to change: `org update` and `org remove`
|
|
6028
|
+
* change it, and `client add --replace` and `client remove` refuse it while its organisation has a record.
|
|
5872
6029
|
*
|
|
5873
|
-
*
|
|
5874
|
-
* read its draft — classified for that action (`approve`, `claim`, `revoke`), so the refusal is the one the action
|
|
5875
|
-
* would get. A revocation the classification derives for it is written then, as the action itself would write it:
|
|
5876
|
-
* the first locked action persists it, and this is that action's first step (design 2026-10-05 §D2).
|
|
6030
|
+
* Additive: client rows are loose, so a release that does not know the key keeps it through its writes.
|
|
5877
6031
|
*/
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
6032
|
+
organisation?: string | undefined;
|
|
6033
|
+
}
|
|
6034
|
+
interface InboxConfig {
|
|
6035
|
+
/** Immutable id (`ibx_` + 16 base32 characters). Secrets and all state are keyed by it; the alias is a label. */
|
|
6036
|
+
id: string;
|
|
6037
|
+
provider: string;
|
|
6038
|
+
email: string;
|
|
6039
|
+
/** Stable account id (OIDC `sub`). Absent for inboxes imported from tools that never asked for it. */
|
|
6040
|
+
sub?: string | undefined;
|
|
6041
|
+
identity: "oidc" | "legacy";
|
|
6042
|
+
client: string;
|
|
6043
|
+
tier: string;
|
|
6044
|
+
contacts: boolean;
|
|
6045
|
+
grantedScopes: string[];
|
|
6046
|
+
secretRef: string;
|
|
6047
|
+
sendPolicy?: SendPolicy | undefined;
|
|
6048
|
+
/** Overrides `defaults.changePolicy` for changes to this inbox. */
|
|
6049
|
+
changePolicy?: ChangePolicy | undefined;
|
|
6050
|
+
internalDomains: string[];
|
|
6051
|
+
createdAt: string;
|
|
6052
|
+
}
|
|
6053
|
+
interface Defaults {
|
|
6054
|
+
sendPolicy: SendPolicy;
|
|
5885
6055
|
/**
|
|
5886
|
-
*
|
|
5887
|
-
*
|
|
5888
|
-
*
|
|
5889
|
-
*
|
|
6056
|
+
* How a loosening is approved wherever an inbox or account does not say otherwise. Absent reads as `chat`.
|
|
6057
|
+
*
|
|
6058
|
+
* Absent rather than filled in by the schema, unlike `sendPolicy`. A config written before this setting existed
|
|
6059
|
+
* reads as `chat` from then on, and the release note says so; a default the schema supplied would be written into
|
|
6060
|
+
* the file by the next unrelated change, where it would look like a choice somebody made.
|
|
5890
6061
|
*/
|
|
5891
|
-
|
|
6062
|
+
changePolicy?: ChangePolicy | undefined;
|
|
6063
|
+
riskEscalation: boolean;
|
|
6064
|
+
sendCaps: {
|
|
6065
|
+
perHour: number;
|
|
6066
|
+
perDay: number;
|
|
6067
|
+
};
|
|
6068
|
+
attachRoots: string[];
|
|
6069
|
+
/** Extra deny entries on top of the built-in list. */
|
|
6070
|
+
attachDeny: string[];
|
|
6071
|
+
downloadsDir?: string | undefined;
|
|
6072
|
+
timezone: string;
|
|
6073
|
+
confirm: {
|
|
6074
|
+
/**
|
|
6075
|
+
* Fail-closed allowlist of MCP `clientInfo.name` values whose form elicitation is trusted to reach a human.
|
|
6076
|
+
* Empty by default; only the CLI adds entries, after a probe.
|
|
6077
|
+
*/
|
|
6078
|
+
elicitationClients: string[];
|
|
6079
|
+
};
|
|
5892
6080
|
/**
|
|
5893
|
-
* The
|
|
5894
|
-
*
|
|
5895
|
-
*
|
|
5896
|
-
*
|
|
5897
|
-
*
|
|
6081
|
+
* The daily update check (design 2026-09-28): once a day this machine asks npm whether a newer release is out, and
|
|
6082
|
+
* every server and command stops until it is updated or put off until tomorrow. Absent reads as `on`.
|
|
6083
|
+
*
|
|
6084
|
+
* Absent rather than filled in, as `changePolicy` is: a default the schema supplied would be written into the file
|
|
6085
|
+
* by the next unrelated change, where it would look like a choice somebody made. Turning it off is a loosening —
|
|
6086
|
+
* it is what keeps a machine from quietly falling behind a release — so it is approved like one; on applies at once.
|
|
5898
6087
|
*/
|
|
5899
|
-
|
|
5900
|
-
/** Issues a new challenge to show a human; only its hash is kept. */
|
|
5901
|
-
issueChallenge(approvalId: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
|
|
5902
|
-
usedSaid?: UsedSaid | undefined;
|
|
5903
|
-
}): Promise<string>;
|
|
5904
|
-
/** Issues the typed challenge that a terminal or the trusted desktop app must answer for disclosure. */
|
|
5905
|
-
issueDisclosureChallenge(approvalId: string): Promise<string>;
|
|
6088
|
+
updateCheck?: UpdateCheckSetting | undefined;
|
|
5906
6089
|
/**
|
|
5907
|
-
*
|
|
5908
|
-
*
|
|
6090
|
+
* How this machine updates (design 2026-10-16, U2): `auto`, `every-14-days` or `never`. Absent reads from
|
|
6091
|
+
* `updateCheck` — `on` as `auto`, `off` as `never` — and both absent as `auto`. A word this release does not know
|
|
6092
|
+
* reads as `auto`, and the file stays readable: a later release may add a level.
|
|
6093
|
+
*
|
|
6094
|
+
* Moving to a level that updates less is a loosening, approved like one; moving back applies at once. Every write of
|
|
6095
|
+
* a level writes `updateCheck` beside it, `off` for `never` and `on` otherwise, so a release before 0.20 sharing the
|
|
6096
|
+
* file checks, or does not, as the person chose.
|
|
5909
6097
|
*/
|
|
5910
|
-
|
|
6098
|
+
updates?: string | undefined;
|
|
6099
|
+
}
|
|
6100
|
+
/**
|
|
6101
|
+
* A connected account on a platform that is not mail: a Slack workspace, and every channel after it (design
|
|
6102
|
+
* 2026-09-26) — one generic record, whatever the platform.
|
|
6103
|
+
*
|
|
6104
|
+
* `id`, `platform`, `workspace` (the container: a Slack team, a Resend team, a WhatsApp store), `userId` (who it acts
|
|
6105
|
+
* as), `tier`, `mode`, `grantedScopes`, `secretRef`, the two policies and `createdAt` are every channel's. A channel
|
|
6106
|
+
* may keep keys of its own beside them — Slack's `appId`, `redirectPort` — and they are kept through every write. It
|
|
6107
|
+
* may not add a safety setting: only `sendPolicy`, `changePolicy` and `mode` are judged by `classifyChange`, so a key
|
|
6108
|
+
* of a channel's own that loosened something would loosen it unasked.
|
|
6109
|
+
*
|
|
6110
|
+
* `secretRef` stays required, although the 2026-09-26 contract made it optional for a channel with no credential:
|
|
6111
|
+
* every earlier release requires it, and they share this file — an account without one would make the whole
|
|
6112
|
+
* configuration unreadable to a server started last week. Such a channel records a reference that names no secret,
|
|
6113
|
+
* `<platform>:none:<id>`, until a config version can say otherwise.
|
|
6114
|
+
*
|
|
6115
|
+
* It sits beside `inboxes` rather than replacing it. The spec asked for one `accounts` map holding everything, and
|
|
6116
|
+
* that rename is the one change this file cannot take: `version: 1` is additive precisely because an MCP server
|
|
6117
|
+
* started last week and a CLI run today share the file, and a release that moved every mailbox out of `inboxes`
|
|
6118
|
+
* would read, to the older of the two, as a config with no mailboxes in it. What the rename was for — one list of
|
|
6119
|
+
* everything connected, whatever the platform — is a question about the shape of the answer, not the shape of the
|
|
6120
|
+
* file, so `connectedAccounts()` provides it and the file grows one key.
|
|
6121
|
+
*/
|
|
6122
|
+
interface AccountConfig {
|
|
6123
|
+
id: string;
|
|
6124
|
+
/** `slack`. */
|
|
6125
|
+
platform: string;
|
|
6126
|
+
/** The workspace or team id. Every other id this account sees is only meaningful inside it. */
|
|
6127
|
+
workspace: string;
|
|
6128
|
+
/** "Acme Corp" — shown to people, never matched on: a workspace can be renamed and stays the same workspace. */
|
|
6129
|
+
workspaceName?: string | undefined;
|
|
6130
|
+
/** This account's own user id in that workspace, so its own messages can be told from everyone else's. */
|
|
6131
|
+
userId: string;
|
|
6132
|
+
tier: string;
|
|
6133
|
+
grantedScopes: string[];
|
|
6134
|
+
/** Where its credential is in the secret store, `<platform>:…` — for a channel that stores none, `<platform>:none:<id>`. */
|
|
6135
|
+
secretRef: string;
|
|
6136
|
+
sendPolicy?: SendPolicy | undefined;
|
|
6137
|
+
/** Overrides `defaults.changePolicy` for changes to this account. */
|
|
6138
|
+
changePolicy?: ChangePolicy | undefined;
|
|
6139
|
+
createdAt: string;
|
|
5911
6140
|
/**
|
|
5912
|
-
*
|
|
5913
|
-
*
|
|
5914
|
-
*
|
|
6141
|
+
* The OAuth client this account's token was issued by, and the app it belongs to.
|
|
6142
|
+
*
|
|
6143
|
+
* Recorded because the Gmail release found the opposite: a reauth used the first OAuth client in the config
|
|
6144
|
+
* rather than the inbox's own, and then did not record which one it had used. Slack makes that worse — D8
|
|
6145
|
+
* means **one app per workspace**, so "the first app" is wrong more often than it is right — and a reauth
|
|
6146
|
+
* that silently moves an account onto a different app changes what it can do without saying so.
|
|
6147
|
+
*
|
|
6148
|
+
* Optional because the key is additive: a config written before these existed parses unchanged.
|
|
5915
6149
|
*/
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
}): Promise<DisclosureApprovalRecord>;
|
|
6150
|
+
oauthClientId?: string | undefined;
|
|
6151
|
+
appId?: string | undefined;
|
|
5919
6152
|
/**
|
|
5920
|
-
*
|
|
5921
|
-
* record was prepared for; otherwise the record is voided, because the human would be approving content the record
|
|
5922
|
-
* does not describe. Three wrong answers void it too.
|
|
6153
|
+
* `read` or `send`, as installed.
|
|
5923
6154
|
*
|
|
5924
|
-
*
|
|
5925
|
-
*
|
|
6155
|
+
* Kept beside `grantedScopes` rather than derived from them, because the two answer different questions: the
|
|
6156
|
+
* scopes are what Slack granted, and this is what the person asked for. A disagreement between them is drift
|
|
6157
|
+
* worth reporting, and a value derived from the scopes could never disagree.
|
|
5926
6158
|
*/
|
|
5927
|
-
|
|
5928
|
-
usedSaid?: UsedSaid | undefined;
|
|
5929
|
-
}): Promise<ApprovalRecord>;
|
|
6159
|
+
mode?: string | undefined;
|
|
5930
6160
|
/**
|
|
5931
|
-
*
|
|
5932
|
-
* human can still approve it; integrity failures (another account, an edited or changed draft, different recipients
|
|
5933
|
-
* or subject) void it. Success creates the O_EXCL claim marker.
|
|
6161
|
+
* The loopback port the last sign-in used, which is the one in the app's redirect URL.
|
|
5934
6162
|
*
|
|
5935
|
-
*
|
|
5936
|
-
*
|
|
5937
|
-
*
|
|
6163
|
+
* Slack matches redirect URLs exactly, so every later sign-in and every set of steps that edits the app has to
|
|
6164
|
+
* name this number. Without it the steps said `<port>`, or — from the MCP server — guessed 51234, and a person who
|
|
6165
|
+
* had chosen another port followed them to a sign-in that failed on the way back.
|
|
5938
6166
|
*/
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
6167
|
+
redirectPort?: number | undefined;
|
|
6168
|
+
/** The organisation profile whose Slack app created this account. Absent for a person's own app. */
|
|
6169
|
+
organisation?: string | undefined;
|
|
6170
|
+
/** Which app in that profile issued the account's current credential. */
|
|
6171
|
+
profileApp?: "read" | "send" | undefined;
|
|
6172
|
+
}
|
|
6173
|
+
type PendingRevocationStatus = "pending" | "revoked" | "expired";
|
|
6174
|
+
/** One bearer token in a superseded rotating bundle, tracked independently from its pair. */
|
|
6175
|
+
interface PendingRevocationTokenState {
|
|
6176
|
+
status: PendingRevocationStatus;
|
|
6177
|
+
/** The fixed absolute time after which the old token is known to be unusable. */
|
|
6178
|
+
deadline: string;
|
|
6179
|
+
}
|
|
6180
|
+
/**
|
|
6181
|
+
* A superseded credential kept until each token in it is conclusively revoked or expired.
|
|
6182
|
+
*
|
|
6183
|
+
* `store` belongs to the entry rather than being inferred from `secrets.store`: an older release can move the root
|
|
6184
|
+
* store without knowing to move this bundle, and cleanup must still find the only copy that exists.
|
|
6185
|
+
*/
|
|
6186
|
+
interface PendingRevocation {
|
|
6187
|
+
ref: string;
|
|
6188
|
+
store: StoreKind;
|
|
6189
|
+
platform: string;
|
|
6190
|
+
workspace: string;
|
|
6191
|
+
createdAt: string;
|
|
6192
|
+
tokens: {
|
|
6193
|
+
access: PendingRevocationTokenState;
|
|
6194
|
+
refresh?: PendingRevocationTokenState | undefined;
|
|
6195
|
+
};
|
|
6196
|
+
}
|
|
6197
|
+
interface ConfigBody {
|
|
5944
6198
|
/**
|
|
5945
|
-
*
|
|
5946
|
-
*
|
|
5947
|
-
* else) — a renewal never moves a record back to `sending`. Refused, writing nothing, without the claim's token.
|
|
6199
|
+
* The one secret backend for this config directory: client secrets, refresh tokens and the approval key. Absent
|
|
6200
|
+
* until the first command that stores a secret chooses it.
|
|
5948
6201
|
*/
|
|
5949
|
-
|
|
6202
|
+
secrets?: {
|
|
6203
|
+
store: StoreKind;
|
|
6204
|
+
} | undefined;
|
|
6205
|
+
clients: Record<string, ClientConfig>;
|
|
6206
|
+
inboxes: Record<string, InboxConfig>;
|
|
6207
|
+
/** Non-mail accounts. Absent in every config written before this key existed, hence the default. */
|
|
6208
|
+
accounts: Record<string, AccountConfig>;
|
|
6209
|
+
/** Superseded rotating credentials which still need conclusive per-token cleanup. */
|
|
6210
|
+
pendingRevocations?: PendingRevocation[] | undefined;
|
|
6211
|
+
defaults: Defaults;
|
|
5950
6212
|
/**
|
|
5951
|
-
*
|
|
5952
|
-
*
|
|
5953
|
-
*
|
|
6213
|
+
* How what an agent sends is labelled (design 2026-10-15-agent-label, L5): the word, the name, the rules that say
|
|
6214
|
+
* who a message appears to come from, and each Slack account's agent emoji.
|
|
6215
|
+
*
|
|
6216
|
+
* `unknown` here, on purpose: it is read only through `agentLabelOf`, which reads a value it does not understand
|
|
6217
|
+
* as the stricter one — an unknown word as `agent`, a rule list it cannot read as no rules — so a hand edit or a
|
|
6218
|
+
* later release's key can only tighten. Kept through every write as the schemas keep every key they do not name, and
|
|
6219
|
+
* judged by `classifyChange`.
|
|
5954
6220
|
*/
|
|
5955
|
-
|
|
6221
|
+
agentLabel?: unknown;
|
|
5956
6222
|
/**
|
|
5957
|
-
*
|
|
6223
|
+
* What an agent may do (design 2026-10-19-agent-behaviour-rules, B3): `{ rules: [...] }`, each rule the acts allowed
|
|
6224
|
+
* in one account or one of its conversations.
|
|
5958
6225
|
*
|
|
5959
|
-
*
|
|
5960
|
-
*
|
|
5961
|
-
*
|
|
6226
|
+
* `unknown` here, as `agentLabel` is, and for the same reason the other way round: it is read only through
|
|
6227
|
+
* `readBehaviourSettings`, which reads a value it does not understand as allowing less — an unknown act dropped, a
|
|
6228
|
+
* rule it cannot read as its account allowing nothing — never as no rules. Kept through every write as the schemas
|
|
6229
|
+
* keep every key they do not name, so older releases keep it too, and judged by `classifyChange`.
|
|
5962
6230
|
*/
|
|
5963
|
-
|
|
6231
|
+
behaviour?: unknown;
|
|
6232
|
+
}
|
|
6233
|
+
interface ConfigV1 extends ConfigBody {
|
|
6234
|
+
version: 1;
|
|
6235
|
+
}
|
|
6236
|
+
/** A name an account used to have, and the account that had it. */
|
|
6237
|
+
interface FormerName {
|
|
6238
|
+
/** What it was renamed to — at the time. The account's current name is found by `id`, so a later rename is followed. */
|
|
6239
|
+
name: string;
|
|
6240
|
+
id: string;
|
|
6241
|
+
}
|
|
6242
|
+
/**
|
|
6243
|
+
* Names that were replaced, per kind, and never reusable.
|
|
6244
|
+
*
|
|
6245
|
+
* Per kind because version 1 lets a mailbox and a workspace share a word: `work` the mailbox and `work` the workspace
|
|
6246
|
+
* become `work/gmail` and `work/slack`, and one flat record could not say which old `work` is which.
|
|
6247
|
+
*/
|
|
6248
|
+
interface FormerNames {
|
|
6249
|
+
inboxes: Record<string, FormerName>;
|
|
6250
|
+
accounts: Record<string, FormerName>;
|
|
6251
|
+
}
|
|
6252
|
+
/** Who an organisation's Google client is for, as its administrator states it (design 2026-10-02 §D2). */
|
|
6253
|
+
type OrganisationServes = "any" | {
|
|
6254
|
+
domains: string[];
|
|
6255
|
+
};
|
|
6256
|
+
/**
|
|
6257
|
+
* One Google client an organisation profile brought here: a *generation* (design 2026-10-02 §D4).
|
|
6258
|
+
*
|
|
6259
|
+
* `owned` — the profile made the client row `name` (`rgc-1`), which carries `organisation`, and its secret is the
|
|
6260
|
+
* profile's to rewrite. `adopted` — the row was already here under a name a person chose (they ran `client add` with
|
|
6261
|
+
* the organisation's file), and stays theirs: no `org` command changes or removes it. `serves` is per generation,
|
|
6262
|
+
* because it is a fact about one client, not about the organisation.
|
|
6263
|
+
*/
|
|
6264
|
+
interface OrganisationGeneration {
|
|
6265
|
+
name: string;
|
|
6266
|
+
clientId: string;
|
|
6267
|
+
projectId?: string | undefined;
|
|
6268
|
+
ownership: "owned" | "adopted";
|
|
6269
|
+
serves: OrganisationServes;
|
|
6270
|
+
addedAt: string;
|
|
6271
|
+
}
|
|
6272
|
+
interface OrganisationSlackApp {
|
|
6273
|
+
clientId: string;
|
|
6274
|
+
appId?: string | undefined;
|
|
6275
|
+
}
|
|
6276
|
+
/**
|
|
6277
|
+
* An organisation's update caps, as the file beside its profile names them (design 2026-10-16, U8), and the SHA-256 of
|
|
6278
|
+
* that file, so `org update` can tell the organisation changing them from the record drifting. A `max` word this release
|
|
6279
|
+
* does not know reads as `never`, the strictest it could mean: caps only ever tighten.
|
|
6280
|
+
*/
|
|
6281
|
+
interface OrganisationUpdates {
|
|
6282
|
+
max?: string | undefined;
|
|
6283
|
+
background?: boolean | undefined;
|
|
6284
|
+
sha256: string;
|
|
6285
|
+
}
|
|
6286
|
+
/**
|
|
6287
|
+
* The organisation's bot-only agent app, as its profile's agent-app file names it (CUE-493): the app's id, and the
|
|
6288
|
+
* SHA-256 of that file, so `org update` can tell the organisation changing it from the record drifting. No client id:
|
|
6289
|
+
* nobody signs in through it — its administrator installs it once and its members paste its bot token.
|
|
6290
|
+
*/
|
|
6291
|
+
interface OrganisationAgentApp {
|
|
6292
|
+
appId: string;
|
|
6293
|
+
sha256: string;
|
|
6294
|
+
}
|
|
6295
|
+
/**
|
|
6296
|
+
* What this machine records of one organisation profile: never its bytes and never its secret — where it was read
|
|
6297
|
+
* from, the SHA-256 of what was read and when, the generations of its Google client, and its Slack apps.
|
|
6298
|
+
*/
|
|
6299
|
+
interface OrganisationRecord {
|
|
6300
|
+
label: string;
|
|
6301
|
+
/** Read again, from this absolute path, by every `org update`. `file` is the one kind version 1 knows (§D3). */
|
|
6302
|
+
source: {
|
|
6303
|
+
kind: string;
|
|
6304
|
+
path: string;
|
|
6305
|
+
};
|
|
6306
|
+
sha256: string;
|
|
6307
|
+
readAt: string;
|
|
6308
|
+
addedAt: string;
|
|
6309
|
+
/** Whether the member asked for this organisation's client to serve addresses outside it too (§D6). */
|
|
6310
|
+
forOtherAddresses: boolean;
|
|
6311
|
+
/** `active` names the generation new mailboxes get — none once a profile stops naming a Google client. */
|
|
6312
|
+
gmail?: {
|
|
6313
|
+
active: string | null;
|
|
6314
|
+
generations: OrganisationGeneration[];
|
|
6315
|
+
} | undefined;
|
|
6316
|
+
slack?: {
|
|
6317
|
+
workspace: string;
|
|
6318
|
+
workspaceName: string;
|
|
6319
|
+
redirectPort: number;
|
|
6320
|
+
apps: {
|
|
6321
|
+
read?: OrganisationSlackApp | undefined;
|
|
6322
|
+
send?: OrganisationSlackApp | undefined;
|
|
6323
|
+
agent?: OrganisationAgentApp | undefined;
|
|
6324
|
+
};
|
|
6325
|
+
} | undefined;
|
|
6326
|
+
/** The organisation's update caps, from the file beside its profile; absent when it names none. */
|
|
6327
|
+
updates?: OrganisationUpdates | undefined;
|
|
5964
6328
|
/**
|
|
5965
|
-
*
|
|
5966
|
-
*
|
|
5967
|
-
* `
|
|
5968
|
-
* path, a different value, a different account or a different effect voids the approval, because the person agreed
|
|
5969
|
-
* to something else. `live.policy` is the change policy in force now, and the stricter of it and the one at prepare
|
|
5970
|
-
* decides — so tightening the policy after an agent prepared a change takes effect on that change, and loosening it
|
|
5971
|
-
* does not release one prepared under `confirm`.
|
|
6329
|
+
* The entries of the profile's `extensions` this release did not know and skipped, being optional (design 2026-10-19,
|
|
6330
|
+
* X3): the release that first understands each, and the SHA-256 of the entry's canonical JSON — as
|
|
6331
|
+
* `Record<string, OrganisationIgnoredExtension>`. Kept in step with the profile. Absent when none was skipped.
|
|
5972
6332
|
*
|
|
5973
|
-
*
|
|
5974
|
-
*
|
|
6333
|
+
* Held as the file holds it, whatever its shape, and read only through `ignoredListOf`, which keeps the well-formed
|
|
6334
|
+
* entries and names the rest: 0.21.0 is the first release to read it, so a shape a later release or a hand gave it
|
|
6335
|
+
* must never make the whole configuration unreadable, nor be dropped by an unrelated write. Losing it is safe: a
|
|
6336
|
+
* later release that understands an entry judges applying it by its classifier whether or not the list names it —
|
|
6337
|
+
* a tightening at once, anything else approved first, never a repair of drift (X4).
|
|
5975
6338
|
*/
|
|
5976
|
-
|
|
6339
|
+
ignoredExtensions?: unknown;
|
|
6340
|
+
}
|
|
6341
|
+
interface ConfigV2 extends ConfigBody {
|
|
6342
|
+
version: 2;
|
|
6343
|
+
formerNames: FormerNames;
|
|
5977
6344
|
/**
|
|
5978
|
-
*
|
|
5979
|
-
*
|
|
5980
|
-
*
|
|
5981
|
-
* `policy` is the change policy of the mailbox or workspace the files come from, as it stands when the question is
|
|
5982
|
-
* asked: where a stranger's files land on this machine is a change to it, and is answered the way that account's
|
|
5983
|
-
* other changes are approved. A claim holds the question to the stricter of this and the policy then.
|
|
6345
|
+
* The organisation profiles added here, by organisation word (design 2026-10-02 §D4). Absent reads as none, and
|
|
6346
|
+
* stays absent until the first `org add`: a key the schema filled in would be written into every file by the next
|
|
6347
|
+
* unrelated change.
|
|
5984
6348
|
*/
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
6349
|
+
organisations?: Record<string, OrganisationRecord> | undefined;
|
|
6350
|
+
}
|
|
6351
|
+
/** What a legacy drain records of one tracked record: still open, or what became of it. */
|
|
6352
|
+
type LegacyDrainOutcome = "revoked" | "expired" | "used" | "failed" | "sending" | "unknown";
|
|
6353
|
+
/**
|
|
6354
|
+
* The approvals an earlier release prepared that were still waiting, or already being sent, when this configuration
|
|
6355
|
+
* became version 3, and what has become of each (design 2026-10-05 §D1, "Old releases are locked out before the fence
|
|
6356
|
+
* is relied on").
|
|
6357
|
+
*/
|
|
6358
|
+
interface LegacyDrain {
|
|
6359
|
+
/** When the conversion that opened the drain was written. */
|
|
6360
|
+
since: string;
|
|
6361
|
+
/**
|
|
6362
|
+
* Every tracked version-1 send record by id: `open` until it has an outcome — or `sending` from the start, for one an
|
|
6363
|
+
* earlier release was already sending when the conversion ran.
|
|
6364
|
+
*/
|
|
6365
|
+
tracked: Record<string, "open" | LegacyDrainOutcome>;
|
|
6366
|
+
}
|
|
6367
|
+
/** What version 3 adds to a body of either naming. */
|
|
6368
|
+
interface Version3Fields {
|
|
6369
|
+
/** Each owner's send epoch by id; absent reads as 0. Raised by every write that turns its send policy to `never`. */
|
|
6370
|
+
sendEpochs?: Record<string, number> | undefined;
|
|
6371
|
+
/**
|
|
6372
|
+
* The standing epoch (design 2026-10-19-standing-approvals, S8); absent reads as 0. Raised by `standing off` and by
|
|
6373
|
+
* every write that turns a change policy from `chat` to `confirm`, and never lowered: a standing approval made under
|
|
6374
|
+
* an earlier one has ended.
|
|
6375
|
+
*/
|
|
6376
|
+
standingEpoch?: number | undefined;
|
|
6377
|
+
/** Present while records from an earlier release are being retired. */
|
|
6378
|
+
legacyDrain?: LegacyDrain | undefined;
|
|
6379
|
+
}
|
|
6380
|
+
/** Version 3 over a version-1 body: one plain word per account. */
|
|
6381
|
+
interface ConfigV3Naming1 extends ConfigBody, Version3Fields {
|
|
6382
|
+
version: 3;
|
|
6383
|
+
naming: 1;
|
|
6384
|
+
}
|
|
6385
|
+
/** Version 3 over a version-2 body: `organisation/platform` names, and the names they replaced. */
|
|
6386
|
+
interface ConfigV3Naming2 extends Omit<ConfigV2, "version">, Version3Fields {
|
|
6387
|
+
version: 3;
|
|
6388
|
+
naming: 2;
|
|
6389
|
+
}
|
|
6390
|
+
/**
|
|
6391
|
+
* Version 3: the send epoch and the legacy drain (design 2026-10-05 §D1), over either naming — converted from version 1
|
|
6392
|
+
* or 2 without renaming anything, so its body keeps the rules of the version it came from.
|
|
6393
|
+
*/
|
|
6394
|
+
type ConfigV3 = ConfigV3Naming1 | ConfigV3Naming2;
|
|
6395
|
+
type Config$1 = ConfigV1 | ConfigV2 | ConfigV3;
|
|
6396
|
+
/** A configuration whose accounts are named `organisation/platform`: version 2, or version 3 over it. */
|
|
6397
|
+
type NamedConfig = ConfigV2 | ConfigV3Naming2;
|
|
6398
|
+
/** A configuration whose accounts have one plain word each: version 1, or version 3 over it. */
|
|
6399
|
+
type PlainConfig = ConfigV1 | ConfigV3Naming1;
|
|
6400
|
+
/** The committed config returned by an update; process-only commit metadata is deliberately stored out of band. */
|
|
6401
|
+
type ConfigUpdateResult = Config$1;
|
|
6402
|
+
declare class ConfigStore {
|
|
6403
|
+
#private;
|
|
6404
|
+
readonly path: string;
|
|
5993
6405
|
/**
|
|
5994
|
-
*
|
|
5995
|
-
*
|
|
5996
|
-
* the download to claim it — which it may then do under `confirm`, and which saves where this answer says.
|
|
5997
|
-
*
|
|
5998
|
-
* Only a pending question can be answered, and only once: a second answer is refused, not taken over the first.
|
|
6406
|
+
* @param options.handoffs the printing package's handoffs (`core.handoffs`, CUE-403), for the command a refusal
|
|
6407
|
+
* names; left out, a refusal that has to name one is a programming error (`requiredHandoffs`).
|
|
5999
6408
|
*/
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6409
|
+
constructor(configDir: string, options?: {
|
|
6410
|
+
readonly handoffs?: CliHandoffs | undefined;
|
|
6411
|
+
/** Test-only pauses inside the version-3 conversion (`config-hooks.ts`); unreachable from the package root. */
|
|
6412
|
+
readonly [CONVERSION_HOOKS]?: ConversionHooks | undefined;
|
|
6413
|
+
});
|
|
6414
|
+
/** The current config; an empty one when the file does not exist yet. */
|
|
6415
|
+
load(): Promise<Config$1>;
|
|
6003
6416
|
/**
|
|
6004
|
-
*
|
|
6005
|
-
*
|
|
6006
|
-
* answer the person recorded, when they recorded one.
|
|
6007
|
-
*
|
|
6008
|
-
* `live` is the download as the caller computes it now: the same account, the same request, the same files under
|
|
6009
|
-
* the same names — the person answered for those files and no others. Any other is refused, and the question left
|
|
6010
|
-
* open, as it was: a second call that got an argument wrong is the agent's slip, not the person's, and voiding the
|
|
6011
|
-
* question for it would make the person answer again for nothing. It still expires, and is claimed once.
|
|
6012
|
-
*
|
|
6013
|
-
* Whether it can be claimed is the download `claimable` matrix (`downloadClaimable`, design 2026-10-05 §D2), decided
|
|
6014
|
-
* under the question's lock against the live change policy read there — never a policy the caller read before it.
|
|
6015
|
-
* While both the policy the question was asked under and the live one are `chat`, a pending question is claimed
|
|
6016
|
-
* with the answer the caller carries: the person gave it in the conversation, and it is never kept. Otherwise only a
|
|
6017
|
-
* question the person answered at a terminal or in a trusted form can be claimed; a pending one is refused, and left
|
|
6018
|
-
* as it is, so they can still answer it. One used, voided, expired or corrupt is refused for what it is.
|
|
6019
|
-
*
|
|
6020
|
-
* `options.signal` is the download's cancellation, asked under the lock as a send's is (`ClaimOptions.signal`): a
|
|
6021
|
-
* download cancelled while this waited saves nothing and leaves the question open, the answer still unused.
|
|
6417
|
+
* Read-modify-write under a lock, so a CLI command and a running MCP server never lose each other's changes. The
|
|
6418
|
+
* mutator receives a fresh copy read inside the lock and returns the new config, which is validated before writing.
|
|
6022
6419
|
*/
|
|
6023
|
-
|
|
6024
|
-
|
|
6420
|
+
update(mutator: (config: Config$1) => Config$1 | Promise<Config$1>, options?: {
|
|
6421
|
+
consent?: LooseningConsent;
|
|
6025
6422
|
signal?: AbortSignal | undefined;
|
|
6026
|
-
|
|
6027
|
-
handoffs?: CliHandoffs | undefined;
|
|
6028
|
-
}): Promise<ApprovalRecord & {
|
|
6029
|
-
download: DownloadBinding;
|
|
6030
|
-
}>;
|
|
6423
|
+
}): Promise<ConfigUpdateResult>;
|
|
6031
6424
|
/**
|
|
6032
|
-
*
|
|
6033
|
-
* or `failed`.
|
|
6425
|
+
* The one way a version-1 config becomes version 2.
|
|
6034
6426
|
*
|
|
6035
|
-
*
|
|
6036
|
-
*
|
|
6037
|
-
* every later reader would take for one.
|
|
6427
|
+
* Under the credentials lock and then the config lock — the order everything takes them in — so it cannot land
|
|
6428
|
+
* between a removal's read and its write, or in the middle of moving secrets between backends.
|
|
6038
6429
|
*
|
|
6039
|
-
*
|
|
6040
|
-
*
|
|
6041
|
-
*
|
|
6042
|
-
*/
|
|
6043
|
-
complete(approvalId: string, claimToken: string, outcome: SendOutcome): Promise<ApprovalRecord>;
|
|
6044
|
-
/**
|
|
6045
|
-
* Voids a pending or approved record. Other records are left as they are, and returned as they read.
|
|
6430
|
+
* `expected` is the fingerprint of the config the caller previewed. The file is read again inside the locks, and
|
|
6431
|
+
* if it is not that config any more the whole thing is refused: somebody confirmed a mapping computed from
|
|
6432
|
+
* something else. Nothing waits for a person while holding a lock; the preview happens before this is called.
|
|
6046
6433
|
*
|
|
6047
|
-
* `
|
|
6048
|
-
*
|
|
6049
|
-
*
|
|
6050
|
-
* corrupt or unreadable record is never revoked or rewritten: it is refused with the integrity refusal, which repeats
|
|
6051
|
-
* only its stub.
|
|
6052
|
-
*/
|
|
6053
|
-
revoke(approvalId: string, reason: string, options: {
|
|
6054
|
-
disposition: RevokeDisposition;
|
|
6055
|
-
expect?: ApprovalExpectation | undefined;
|
|
6056
|
-
}): Promise<StoredApproval>;
|
|
6057
|
-
/**
|
|
6058
|
-
* Retires a record from an earlier release (version 1) in that release's own shape, so a process still running it
|
|
6059
|
-
* can no longer claim the record — and nothing more.
|
|
6434
|
+
* `build` produces version 2 from the locked snapshot. What it may change is checked rather than trusted: the same
|
|
6435
|
+
* accounts, byte for byte, under new keys — a rename grants nothing, so a build that changed anything else is a
|
|
6436
|
+
* bug and is refused before it is written.
|
|
6060
6437
|
*
|
|
6061
|
-
*
|
|
6062
|
-
*
|
|
6063
|
-
*
|
|
6064
|
-
*
|
|
6065
|
-
* either. Any other derived state is returned as it reads and written nothing: an expired record is never rewritten,
|
|
6066
|
-
* as `revoke` leaves every finished record as it is.
|
|
6438
|
+
* Idempotent, but only for this plan. A retry after a write that committed — even one whose lock release then
|
|
6439
|
+
* failed — finds version 2, recognises its own mapping in it, and says so. Version 2 that does *not* carry this
|
|
6440
|
+
* mapping is somebody else's migration; saying "already migrated" there would report a mapping nobody applied,
|
|
6441
|
+
* and a caller updating registrations from it would point them at names that do not exist.
|
|
6067
6442
|
*
|
|
6068
|
-
*
|
|
6069
|
-
*
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
/**
|
|
6073
|
-
* Every approval file, each looked at under its own lock and classified there, as `inspect` looks at one — against one
|
|
6074
|
-
* configuration snapshot, loaded once before any file is read (design 2026-10-05 §D2, §D8). What a list shows: each
|
|
6075
|
-
* record's form and its outcome, nothing skipped. Like a look, it writes only what reading derives — an expiry at its
|
|
6076
|
-
* boundary, a stale send's `unknown` — never a revocation: an owner removed or a stale epoch is shown as revoked, and
|
|
6077
|
-
* written by the next action.
|
|
6443
|
+
* `rows` is the plan, and the question is asked of the mapping rather than of the whole file: between a
|
|
6444
|
+
* committed write and its retry, something else may have changed a policy or a timezone, and a retry refused
|
|
6445
|
+
* over that would be idempotency in name only. The rows are checked here rather than by whoever built them,
|
|
6446
|
+
* for the same reason `build` is: a caller that could answer its own question could answer it wrongly.
|
|
6078
6447
|
*
|
|
6079
|
-
*
|
|
6080
|
-
* `states` keeps those whose classified state is listed — a stored state, `corrupt`, and for a download's answered
|
|
6081
|
-
* question its stored `approved` or `used`.
|
|
6448
|
+
* The file it replaces is copied beside it first — see `backUpBeforeMigration` — and the copy's path returned.
|
|
6082
6449
|
*/
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
outcome: ApprovalOutcome;
|
|
6089
|
-
}>>;
|
|
6450
|
+
migrateNames(expected: string, rows: readonly RenamedAccount[], build: (current: PlainConfig) => NamedConfig): Promise<{
|
|
6451
|
+
status: "migrated" | "already-migrated";
|
|
6452
|
+
config: NamedConfig;
|
|
6453
|
+
backup?: string;
|
|
6454
|
+
}>;
|
|
6090
6455
|
/**
|
|
6091
|
-
*
|
|
6092
|
-
*
|
|
6456
|
+
* The one way a configuration becomes version 3 (design 2026-10-05 §D1, "Old releases are locked out before the
|
|
6457
|
+
* fence is relied on").
|
|
6093
6458
|
*
|
|
6094
|
-
*
|
|
6095
|
-
*
|
|
6459
|
+
* 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
|
|
6460
|
+
* 0.13 process can start a prepare, a claim, an approval or a policy change through this file. Nothing is renamed:
|
|
6461
|
+
* a version-1 body stays version 1's (`naming: 1`) until the names migration, which needs its own approval.
|
|
6462
|
+
*
|
|
6463
|
+
* Under the config lock, the file is read again — never from the cache, never from the read before the lock — and an
|
|
6464
|
+
* existing version 3 returned unchanged: a second converter that lost the race keeps the winner's epochs and drain.
|
|
6465
|
+
* `scan` runs under the same lock, so a record written while this waited for it is seen, and an update cannot land
|
|
6466
|
+
* between the scan and the write; the version-3 write carries `legacyDrain` only when the scan tracked something.
|
|
6467
|
+
* With no config file at all there is nothing to convert, and nothing is written.
|
|
6096
6468
|
*/
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
//#region src/ledger.d.ts
|
|
6104
|
-
interface Caps {
|
|
6105
|
-
perHour: number;
|
|
6106
|
-
perDay: number;
|
|
6107
|
-
}
|
|
6108
|
-
interface CapStatus {
|
|
6109
|
-
hour: number;
|
|
6110
|
-
day: number;
|
|
6111
|
-
/** When the next slot frees up, if a cap is reached. */
|
|
6112
|
-
resetAt?: string;
|
|
6469
|
+
convertToVersion3(scan: () => Promise<LegacyDrain["tracked"]>, options?: {
|
|
6470
|
+
now?: (() => Date) | undefined;
|
|
6471
|
+
}): Promise<{
|
|
6472
|
+
status: "converted" | "already" | "empty";
|
|
6473
|
+
config: Config$1;
|
|
6474
|
+
}>;
|
|
6113
6475
|
}
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
readonly
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
/** Reserves a slot or refuses with APPROVAL_REQUIRED and the time the cap resets. Atomic across processes. */
|
|
6121
|
-
reserve(inboxId: string, approvalId: string, caps: Caps): Promise<CapStatus>;
|
|
6122
|
-
/** Releases a reservation whose send did not happen. */
|
|
6123
|
-
release(inboxId: string, approvalId: string): Promise<void>;
|
|
6476
|
+
/** One account's rename, as the migration planned it. */
|
|
6477
|
+
interface RenamedAccount {
|
|
6478
|
+
readonly kind: "inbox" | "account";
|
|
6479
|
+
readonly from: string;
|
|
6480
|
+
readonly to: string;
|
|
6481
|
+
readonly id: string;
|
|
6124
6482
|
}
|
|
6125
|
-
|
|
6126
|
-
|
|
6483
|
+
/** A value a loosened setting had or will have: a policy, a mode, a list, a pair of caps, a path, or nothing. */
|
|
6484
|
+
type SettingValue = string | number | boolean | readonly string[] | {
|
|
6485
|
+
readonly [key: string]: unknown;
|
|
6486
|
+
} | null;
|
|
6127
6487
|
/**
|
|
6128
|
-
*
|
|
6129
|
-
*
|
|
6130
|
-
*
|
|
6488
|
+
* One loosened setting, with the values the classifier compared.
|
|
6489
|
+
*
|
|
6490
|
+
* The values are the ones it judged, not the raw fields: an inbox that inherits a looser default reports the policy
|
|
6491
|
+
* it inherits, and a new inbox reports the default it was measured against. A preview built from anything else could
|
|
6492
|
+
* show a person one change while the classifier judged another.
|
|
6131
6493
|
*/
|
|
6132
|
-
interface
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
paramsDigest: string;
|
|
6138
|
-
/** Digest of the sorted id list. */
|
|
6139
|
-
idsDigest: string;
|
|
6140
|
-
count: number;
|
|
6141
|
-
createdAt: string;
|
|
6142
|
-
expiresAt: string;
|
|
6143
|
-
}
|
|
6144
|
-
declare class PlanStore {
|
|
6145
|
-
#private;
|
|
6146
|
-
readonly directory: string;
|
|
6147
|
-
constructor(stateDir: string, now?: () => Date);
|
|
6148
|
-
create(input: {
|
|
6149
|
-
inboxId: string;
|
|
6150
|
-
operation: string;
|
|
6151
|
-
params: unknown;
|
|
6152
|
-
ids: readonly string[];
|
|
6153
|
-
}): Promise<PlanRecord>;
|
|
6494
|
+
interface Loosening {
|
|
6495
|
+
/** As `ConfigStore.update` names it in a refusal: `accounts.rgc/slack.mode`, `defaults.sendPolicy`. */
|
|
6496
|
+
readonly path: string;
|
|
6497
|
+
readonly before: SettingValue;
|
|
6498
|
+
readonly after: SettingValue;
|
|
6154
6499
|
/**
|
|
6155
|
-
*
|
|
6156
|
-
*
|
|
6500
|
+
* The inbox or account measured on the before side, by id. Absent for one new in this change and for a setting
|
|
6501
|
+
* that belongs to the whole configuration. Carried so that an approval of this loosening cannot be spent on a
|
|
6502
|
+
* different account that took the same name in the meantime.
|
|
6157
6503
|
*/
|
|
6158
|
-
|
|
6159
|
-
inboxId: string;
|
|
6160
|
-
operation: string;
|
|
6161
|
-
params: unknown;
|
|
6162
|
-
ids: readonly string[];
|
|
6163
|
-
}): Promise<PlanRecord>;
|
|
6504
|
+
readonly id?: string | undefined;
|
|
6164
6505
|
}
|
|
6165
|
-
//#endregion
|
|
6166
|
-
//#region src/secrets.d.ts
|
|
6167
6506
|
/**
|
|
6168
|
-
*
|
|
6169
|
-
* in config; the choice is never switched at runtime and there is no silent fallback from one to the other.
|
|
6507
|
+
* One setting a change writes, from the value in the file before to the value after — whichever way it moves.
|
|
6170
6508
|
*
|
|
6171
|
-
*
|
|
6172
|
-
*
|
|
6173
|
-
*
|
|
6509
|
+
* Unlike a `Loosening`, these are the raw values, not the effective ones: `null` for a setting the file does not
|
|
6510
|
+
* hold, so a mailbox set to `chat` itself is told apart from one inheriting `chat`, which only the first survives a
|
|
6511
|
+
* later change to the default. `id` is the inbox or account measured on the before side, as for a loosening.
|
|
6174
6512
|
*/
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
readonly
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6513
|
+
interface SettingChange {
|
|
6514
|
+
readonly path: string;
|
|
6515
|
+
readonly before: SettingValue;
|
|
6516
|
+
readonly after: SettingValue;
|
|
6517
|
+
readonly id?: string | undefined;
|
|
6518
|
+
}
|
|
6519
|
+
/**
|
|
6520
|
+
* Proof that exactly these paths may loosen: produced by a CLI after a person at a terminal typed a challenge, or by
|
|
6521
|
+
* `claimChange` from a change approval a person gave.
|
|
6522
|
+
*/
|
|
6523
|
+
interface LooseningConsent {
|
|
6524
|
+
kind: "loosening-consent";
|
|
6525
|
+
paths: readonly string[];
|
|
6183
6526
|
/**
|
|
6184
|
-
*
|
|
6527
|
+
* What each path was approved to move between, when the consent came from a change approval.
|
|
6185
6528
|
*
|
|
6186
|
-
*
|
|
6187
|
-
*
|
|
6188
|
-
*
|
|
6189
|
-
*
|
|
6529
|
+
* A terminal challenge is answered moments before the write it permits, so the paths are enough. An approval is
|
|
6530
|
+
* prepared first and applied later — after a sign-in, possibly minutes later — and in between the configuration
|
|
6531
|
+
* can move underneath it: the same path, loosened from a different value, or on a different account that took
|
|
6532
|
+
* the name. With these, `ConfigStore.update` refuses any loosening that is not exactly one of them.
|
|
6190
6533
|
*/
|
|
6191
|
-
|
|
6534
|
+
changes?: readonly Loosening[] | undefined;
|
|
6192
6535
|
}
|
|
6193
6536
|
//#endregion
|
|
6194
|
-
//#region src/
|
|
6195
|
-
/**
|
|
6196
|
-
* Per-inbox runtime facts that any process may update (last successful refresh, last use, health). Kept out of
|
|
6197
|
-
* config.json so a running server never rewrites user intent — a stale in-memory copy of config written back by a
|
|
6198
|
-
* server could otherwise silently undo a policy the user just tightened.
|
|
6199
|
-
*/
|
|
6200
|
-
interface InboxRuntimeState {
|
|
6201
|
-
lastRefreshOkAt?: string | undefined;
|
|
6202
|
-
lastUsedAt?: string | undefined;
|
|
6203
|
-
/** Set to `undefined` to clear it: the merged value is dropped when the file is written. */
|
|
6204
|
-
lastError?: {
|
|
6205
|
-
code: string;
|
|
6206
|
-
message: string;
|
|
6207
|
-
at: string;
|
|
6208
|
-
} | undefined;
|
|
6209
|
-
grantedScopes?: string[] | undefined;
|
|
6210
|
-
refreshTokenExpiresAt?: string | undefined;
|
|
6211
|
-
}
|
|
6212
|
-
declare class InboxStateStore {
|
|
6213
|
-
#private;
|
|
6214
|
-
readonly directory: string;
|
|
6215
|
-
constructor(stateDir: string);
|
|
6216
|
-
get(inboxId: string): Promise<InboxRuntimeState>;
|
|
6217
|
-
/** Merges `patch` into the stored state under a per-inbox lock. */
|
|
6218
|
-
update(inboxId: string, patch: Partial<InboxRuntimeState>): Promise<InboxRuntimeState>;
|
|
6219
|
-
}
|
|
6537
|
+
//#region src/cli-command.d.ts
|
|
6220
6538
|
/**
|
|
6221
|
-
*
|
|
6539
|
+
* The command a person is told to run for one of this suite's own CLIs (design 2026-10-04, D1 and D5; CUE-403).
|
|
6222
6540
|
*
|
|
6223
|
-
*
|
|
6224
|
-
*
|
|
6225
|
-
*
|
|
6226
|
-
*
|
|
6541
|
+
* A bare `agent-gmail approve …` is not runnable for most of the people it is printed for: a package run by npx, by a
|
|
6542
|
+
* managed runtime or from a checkout puts nothing on their PATH. So every such command names this process's Node and
|
|
6543
|
+
* a checked file of the product it runs, with the suite directories it uses pinned as options — and when no file can
|
|
6544
|
+
* be checked, there is no command at all, only a sentence saying what is missing. The caller says which package it is
|
|
6545
|
+
* and what to run; it never supplies the program or quotes the line.
|
|
6227
6546
|
*
|
|
6228
|
-
*
|
|
6229
|
-
*
|
|
6547
|
+
* Three directions. A product's own CLI is found from the product itself, walking from one of its own modules to its
|
|
6548
|
+
* manifest: no registration is ever asked. A channel's handoff to core is found the way the channel itself finds core,
|
|
6549
|
+
* through its installed dependency, and only the same release. Another product's command is found among the servers
|
|
6550
|
+
* registered with the person's MCP clients, which the caller scanned and passes in: only a file-backed registration of
|
|
6551
|
+
* exactly the printing package's version, checked on disk. Nothing a registration names is run, or asked anything —
|
|
6552
|
+
* not even its interpreter for a version: no installer wrote down where a scanned entry came from, so none is trusted
|
|
6553
|
+
* to run (D1). This module never scans, spawns or probes.
|
|
6554
|
+
*
|
|
6555
|
+
* What is checked is checked when the command is printed, not for ever (D6): the file it names is real, readable and
|
|
6556
|
+
* inside its package then, and a later change to that installation is a later fact.
|
|
6230
6557
|
*/
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
platform: string;
|
|
6234
|
-
/** The workspace or team the id belongs to — a Slack team id. */
|
|
6235
|
-
scope: string;
|
|
6236
|
-
/** `U024BE7LH`, `C0123`, `D0456`. Passed in the platform's own canonical form; core does not know its shape. */
|
|
6237
|
-
id: string;
|
|
6238
|
-
}
|
|
6239
|
-
type TaintSource = "header" | "body";
|
|
6240
|
-
/** Why the content reached this process. This is separate from `TaintSource`, which remains the cap priority. */
|
|
6241
|
-
type TaintOrigin = "read" | "event";
|
|
6242
|
-
interface TaintObservation {
|
|
6243
|
-
address: string;
|
|
6244
|
-
source: TaintSource;
|
|
6245
|
-
/** Old readers did not record this, so an omitted origin remains an ordinary read. */
|
|
6246
|
-
origin?: TaintOrigin | undefined;
|
|
6247
|
-
inboxId: string;
|
|
6248
|
-
messageId?: string | undefined;
|
|
6249
|
-
}
|
|
6250
|
-
/** The same observation for a handle. `inboxId` is the account that did the reading, as it is for an address. */
|
|
6251
|
-
interface TaintHandleObservation {
|
|
6252
|
-
handle: TaintHandle;
|
|
6253
|
-
source: TaintSource;
|
|
6254
|
-
/** Old readers did not record this, so an omitted origin remains an ordinary read. */
|
|
6255
|
-
origin?: TaintOrigin | undefined;
|
|
6256
|
-
inboxId: string;
|
|
6257
|
-
messageId?: string | undefined;
|
|
6258
|
-
}
|
|
6259
|
-
/** What is never recorded: the user's own addresses and the domains they call internal. */
|
|
6260
|
-
interface TaintExclusions {
|
|
6261
|
-
ownAddresses: readonly string[];
|
|
6262
|
-
internalDomains: readonly string[];
|
|
6263
|
-
/**
|
|
6264
|
-
* Handles that are never recorded: the account's own user id, and whoever the caller treats as internal — the
|
|
6265
|
-
* counterpart of `internalDomains`, decided per platform because "internal" means a domain for mail and
|
|
6266
|
-
* workspace membership for Slack, and core should not be the thing that knows the difference.
|
|
6267
|
-
*/
|
|
6268
|
-
ownHandles?: readonly TaintHandle[] | undefined;
|
|
6269
|
-
}
|
|
6558
|
+
/** The gate a constructor call has to pass: only this module holds it. */
|
|
6559
|
+
declare const GATE: unique symbol;
|
|
6270
6560
|
/**
|
|
6271
|
-
*
|
|
6272
|
-
*
|
|
6273
|
-
*
|
|
6561
|
+
* A command that runs one of this suite's own CLIs, made only by `locateCliCommand`. Rendered as every printed command
|
|
6562
|
+
* is — a line to paste, or on Windows its words as JSON when no line is safe in every shell (`inlineCommand`,
|
|
6563
|
+
* `commandText`).
|
|
6564
|
+
*
|
|
6565
|
+
* A class with a private field, so the type is nominal: an object literal with the same fields is not one, and nor is
|
|
6566
|
+
* a spread copy of one with other words in it, as it would be with a symbol-keyed brand. Only the type is exported;
|
|
6567
|
+
* the class and its gate are not.
|
|
6274
6568
|
*/
|
|
6275
|
-
|
|
6276
|
-
readonly at: string;
|
|
6277
|
-
readonly source: TaintSource;
|
|
6278
|
-
readonly inboxIds: readonly string[];
|
|
6279
|
-
/** Sorted, duplicate-free provenance. A base entry with no sidecar record is a legacy read. */
|
|
6280
|
-
readonly origins: readonly TaintOrigin[];
|
|
6281
|
-
}
|
|
6282
|
-
interface TaintCheck {
|
|
6283
|
-
address: boolean;
|
|
6284
|
-
domain: boolean;
|
|
6285
|
-
/** The exact address's aggregate, when it is in the window. */
|
|
6286
|
-
addressSeen?: TaintAggregate | undefined;
|
|
6287
|
-
/** Its domain's, when that counts — not a public mailbox provider — and is in the window. */
|
|
6288
|
-
domainSeen?: TaintAggregate | undefined;
|
|
6289
|
-
}
|
|
6290
|
-
/** Testable interruption point for the crash-safe, two-lock sidecar protocol. */
|
|
6291
|
-
interface TaintStoreHooks {
|
|
6292
|
-
afterOriginsCommit?: (() => Promise<void>) | undefined;
|
|
6293
|
-
onLockAcquired?: ((lock: "origins" | "taint" | "handles") => void) | undefined;
|
|
6294
|
-
}
|
|
6295
|
-
declare class TaintStore {
|
|
6569
|
+
declare class PrintedCommand {
|
|
6296
6570
|
#private;
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
/**
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6571
|
+
/** The words, as the program is to receive them: this Node, its flags, the entry, then the CLI's own words. */
|
|
6572
|
+
readonly words: readonly string[];
|
|
6573
|
+
/** The line to paste; null when no supported common or labelled shell rendering is safe. */
|
|
6574
|
+
readonly line: string | null;
|
|
6575
|
+
readonly platform: NodeJS.Platform;
|
|
6576
|
+
readonly shell?: "powershell" | undefined;
|
|
6577
|
+
constructor(gate: typeof GATE, words: readonly string[], platform: NodeJS.Platform, entry: string);
|
|
6304
6578
|
/**
|
|
6305
|
-
*
|
|
6306
|
-
*
|
|
6579
|
+
* The real path of the file it runs, as it was checked when the command was located: a fact about then (D6). The
|
|
6580
|
+
* command names this path and nothing else, so what runs is whatever is there when the person runs it.
|
|
6307
6581
|
*/
|
|
6308
|
-
|
|
6582
|
+
get entry(): string;
|
|
6309
6583
|
/**
|
|
6310
|
-
*
|
|
6311
|
-
*
|
|
6312
|
-
*
|
|
6313
|
-
* carries a domain: two ids sharing a workspace says nothing about either of them.
|
|
6584
|
+
* As JSON — a result's field written out by `--json`, or returned to an MCP client — it is the text it always was:
|
|
6585
|
+
* its line, or its words as JSON with what to do when no Windows line is safe (`commandText`). Rendered where it
|
|
6586
|
+
* leaves the process, so a field can hold the command itself and the wire stays the same.
|
|
6314
6587
|
*/
|
|
6315
|
-
|
|
6316
|
-
/**
|
|
6317
|
-
|
|
6588
|
+
toJSON(): string;
|
|
6589
|
+
/** Never interpolated: a sentence gives a command through `handoffSentence`, a value through `handoffText`. */
|
|
6590
|
+
[Symbol.toPrimitive](): never;
|
|
6318
6591
|
}
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6592
|
+
/** The package asking: a module of its own (`import.meta.url`), and the package that module is expected to be in. */
|
|
6593
|
+
interface CliCommandCaller {
|
|
6594
|
+
readonly url: string;
|
|
6595
|
+
readonly packageName: string;
|
|
6596
|
+
}
|
|
6597
|
+
/** Why there is no command: the step that found nothing it could check. */
|
|
6598
|
+
type CliNotLocatedReason = "caller" | "manifest" | "core" | "version" | "engine" | "bin" | "entry" | "interpreter" | "arguments" | "direction" | "no-registrations" | "not-registered";
|
|
6599
|
+
/** Why one registration of the product was not used. */
|
|
6600
|
+
type RegistrationRejection = "npx" | "arguments" | "no-file" | "no-package" | "version" | "engine" | "bin" | "entry";
|
|
6601
|
+
interface RegistrationNotUsed {
|
|
6602
|
+
readonly client: string;
|
|
6603
|
+
readonly path: string;
|
|
6604
|
+
readonly name: string;
|
|
6605
|
+
readonly why: RegistrationRejection;
|
|
6606
|
+
}
|
|
6607
|
+
/**
|
|
6608
|
+
* No command: nothing to run, and a sentence naming the product, its package and the exact version it needs. An
|
|
6609
|
+
* installation that has gone or changed since is said so, never replaced by a suite command found by name on PATH.
|
|
6610
|
+
*/
|
|
6611
|
+
interface CliCommandNotLocated {
|
|
6612
|
+
readonly ok: false;
|
|
6613
|
+
readonly reason: CliNotLocatedReason;
|
|
6614
|
+
/** How the product is named to a person: `Gmail`. */
|
|
6615
|
+
readonly product: string;
|
|
6616
|
+
readonly package: string;
|
|
6617
|
+
/** The exact version needed: the printing package's own, since the suite is released together. */
|
|
6618
|
+
readonly version: string;
|
|
6619
|
+
/** For `engine`: the Node range the target declares. */
|
|
6620
|
+
readonly nodeRange?: string | undefined;
|
|
6621
|
+
/** For another product: each registration of it that was looked at, and why it was not used. */
|
|
6622
|
+
readonly registrations?: readonly RegistrationNotUsed[] | undefined;
|
|
6623
|
+
/** What was found, in a clause. */
|
|
6624
|
+
readonly detail: string;
|
|
6625
|
+
/** The whole sentence to show. */
|
|
6626
|
+
readonly message: string;
|
|
6342
6627
|
}
|
|
6343
6628
|
//#endregion
|
|
6344
6629
|
//#region ../core/dist/index.d.mts
|