otto-intel-mcp 0.1.2 → 0.1.3
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/README.md +4 -2
- package/dist/adapter/cdp-signer.d.ts +44 -5
- package/dist/adapter/cdp-signer.js +168 -18
- package/dist/adapter/evm-call-failure.d.ts +29 -0
- package/dist/adapter/evm-call-failure.js +109 -0
- package/dist/adapter/refusal.d.ts +1 -1
- package/dist/adapter/refusal.js +13 -0
- package/dist/adapter/route-simulator.d.ts +91 -0
- package/dist/adapter/route-simulator.js +143 -0
- package/dist/executable-equities.d.ts +55 -0
- package/dist/executable-equities.js +116 -0
- package/dist/executable-yield-markets.d.ts +261 -0
- package/dist/executable-yield-markets.js +154 -0
- package/dist/execution-delegated-definition.d.ts +1 -0
- package/dist/execution-delegated-definition.js +13 -2
- package/dist/execution-delegation-admin-definition.d.ts +126 -5
- package/dist/execution-delegation-admin-definition.js +154 -5
- package/dist/execution-delegation-admin.d.ts +48 -2
- package/dist/execution-delegation-admin.js +314 -12
- package/dist/execution-delegation-cap-store.d.ts +369 -0
- package/dist/execution-delegation-cap-store.js +1083 -0
- package/dist/execution-delegation-policy.d.ts +153 -6
- package/dist/execution-delegation-policy.js +260 -16
- package/dist/execution-delegation.d.ts +35 -0
- package/dist/execution-delegation.js +224 -9
- package/dist/execution-errors.d.ts +1 -1
- package/dist/execution-errors.js +98 -0
- package/dist/execution-index.d.ts +3 -2
- package/dist/execution-index.js +3 -2
- package/dist/execution-registration.d.ts +51 -2
- package/dist/execution-registration.js +45 -2
- package/dist/execution-tools.d.ts +44 -1
- package/dist/execution-tools.js +157 -13
- package/dist/lifi-execution-client.d.ts +6 -0
- package/dist/lifi-execution-client.js +3 -0
- package/dist/stock-buy-definition.d.ts +338 -0
- package/dist/stock-buy-definition.js +127 -0
- package/dist/stock-buy-oracle.d.ts +8 -0
- package/dist/stock-buy-oracle.js +22 -0
- package/dist/stock-buy.d.ts +17 -0
- package/dist/stock-buy.js +164 -0
- package/dist/tool-definitions.js +1 -1
- package/dist/yield-deposit-definition.d.ts +323 -0
- package/dist/yield-deposit-definition.js +121 -0
- package/dist/yield-deposit.d.ts +16 -0
- package/dist/yield-deposit.js +141 -0
- package/dist/yield-withdraw-definition.d.ts +306 -0
- package/dist/yield-withdraw-definition.js +88 -0
- package/dist/yield-withdraw.d.ts +21 -0
- package/dist/yield-withdraw.js +120 -0
- package/package.json +28 -4
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* execution-delegation-cap-store.ts — the durable, TRANSACTIONAL state machine behind a person's chosen
|
|
3
|
+
* per-swap cap (Founder word `delegate cap choice`, 2026-09-02; ROUND 5, after four codex NO-GOs).
|
|
4
|
+
*
|
|
5
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* 🔴 THE THREAT MODEL THIS ENFORCES (Founder ruling, 2026-09-03, verbatim)
|
|
7
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════════
|
|
8
|
+
* "The per-person delegation cap protects every delegated send that goes through Otto's page
|
|
9
|
+
* (/app/delegate) and Otto's CLI (otto-execute delegate). A permission a person mints or revokes
|
|
10
|
+
* directly against Coinbase with their own login, outside Otto's page and CLI, is OUT OF SCOPE: Otto
|
|
11
|
+
* cannot distinguish it (CDP returns no grant identity, only an expiry) and does not defend against it;
|
|
12
|
+
* the CDP project-scope $5,000 ceiling remains the hard backstop for every permission. The delegated
|
|
13
|
+
* agent never holds the person's login."
|
|
14
|
+
*
|
|
15
|
+
* Everything below is built to be correct INSIDE that model. Where a defect is only reachable by a
|
|
16
|
+
* direct-to-Coinbase mint or revoke, the behaviour is stated rather than defended — and every one of
|
|
17
|
+
* those paths still fails CLOSED here (an unrecorded or unconfirmed grant authorises nothing at all),
|
|
18
|
+
* so the out-of-scope actor can shorten what Otto may do, never widen it.
|
|
19
|
+
*
|
|
20
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
* 🔴 WHY THE MACHINE, AND WHY IT IS SQLITE
|
|
22
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
* Round 3 made the expiry unique among ROWS and thought that was enough. The gate's two CRITICALs:
|
|
24
|
+
*
|
|
25
|
+
* "The durable row still authenticates an expiry, not a grant. Enforcement requires only
|
|
26
|
+
* (userId, grant.expiresAt); it does not require a successful confirmation, the reservation's
|
|
27
|
+
* mintKey, or an active-grant pointer."
|
|
28
|
+
* "Confirmation is not an enforcement gate. Reserve writes the complete enforceable capability;
|
|
29
|
+
* confirm writes no state at all."
|
|
30
|
+
*
|
|
31
|
+
* Both are the same defect: a reserved row was a BEARER CAPABILITY keyed by an instant. Round 4 took the
|
|
32
|
+
* gate's own minimum safe model —
|
|
33
|
+
*
|
|
34
|
+
* reserved ──confirm──▶ confirmed_active ──┬──▶ superseded (a later confirm won)
|
|
35
|
+
* │ ├──▶ revoked (the person, or a mismatch)
|
|
36
|
+
* └──10 min──▶ expired └──▶ (expiry passes)
|
|
37
|
+
*
|
|
38
|
+
* — and made ONE thing authoritative: `readActive`. A reserved row authorises NOTHING; the send gate
|
|
39
|
+
* reads the confirmed-active record and requires its expiry to equal the live grant's.
|
|
40
|
+
*
|
|
41
|
+
* Round 4 implemented that on filesystem primitives (`open(…,'wx')` + `rename()`), and round 4's gate
|
|
42
|
+
* retired them: *"`rename()` gives atomic visibility of one pointer, not durability or a multi-file
|
|
43
|
+
* transaction: there is no file/directory `fsync`, and confirmation performs three writes … There is
|
|
44
|
+
* also no confirm CAS/serialization. My concurrent-confirm probe returned both calls as `confirmed` and
|
|
45
|
+
* left both reservation files `confirmed_active`, although only one `active.json` won."* Its prescription
|
|
46
|
+
* was explicit: *"Use a transactional shared store with unique `(userId, expiry)` and `(userId, mintKey)`
|
|
47
|
+
* constraints, an active-grant CAS, and atomic confirm/revoke transitions. A database transaction is the
|
|
48
|
+
* simpler safe implementation."*
|
|
49
|
+
*
|
|
50
|
+
* So the primitives are now SQL, on one database file on the mounted Railway volume:
|
|
51
|
+
*
|
|
52
|
+
* · `PRIMARY KEY (user_id, mint_key)` — the mint key is claimed exactly once. Idempotent reserve.
|
|
53
|
+
* · `UNIQUE (user_id, expires_at_ms)` — the reservation instant is claimed exactly once, ACROSS
|
|
54
|
+
* processes, by the database rather than by a directory entry.
|
|
55
|
+
* · `CHECK (state IN …)` — a record can never hold a state this binary does not know.
|
|
56
|
+
* · `active_grants` — ONE ROW PER USER, the only pointer enforcement reads, moved by a
|
|
57
|
+
* COMPARE-AND-SWAP (`UPDATE … WHERE mint_key = <prior> AND expires_at_ms = <prior>`) inside the
|
|
58
|
+
* same transaction that promotes the reservation.
|
|
59
|
+
* · Every transition is a CONDITIONAL UPDATE whose `changes` count decides the winner
|
|
60
|
+
* (`UPDATE … WHERE state = 'reserved'`), so a concurrent double confirm has exactly one winner and
|
|
61
|
+
* the loser is told it was superseded — it is not a race that both sides win.
|
|
62
|
+
* · `BEGIN IMMEDIATE` takes the write lock before the first read, so a reserve/confirm/revoke is
|
|
63
|
+
* serialized against every other writer on the same file, in this process or another.
|
|
64
|
+
* · `journal_mode = WAL`, `synchronous = FULL` — a committed transition survives a crash and a power
|
|
65
|
+
* loss, which is the durability `rename()` did not give.
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ THE PREMISE, STATED. One database file, reached by every worker: `numReplicas: 1` and one mounted
|
|
68
|
+
* volume. SQLite's locking is per file, so two volumes would be two independent stores — and two
|
|
69
|
+
* independent stores can each confirm a different cap for one person, which is the one shape that could
|
|
70
|
+
* widen a permission from inside the model. Nothing inside a process can check this; the deploy does
|
|
71
|
+
* (`railway.json`), and the staged first rollout below is what proves it before minting is even possible.
|
|
72
|
+
*
|
|
73
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
* 🔴 WHY `node:sqlite` AND NOT `better-sqlite3`
|
|
75
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════════
|
|
76
|
+
* `better-sqlite3` installs fine on this image (verified: a prebuilt linux-x64 / Node 22 binary, no
|
|
77
|
+
* compile). It is not a Railway problem. It is a PUBLISHED-PACKAGE problem: `otto-intel-mcp` ships to npm
|
|
78
|
+
* and is run as `npx otto-intel-mcp`, and `services/otto-execute` vendors it file-for-file at build time.
|
|
79
|
+
* A hard native dependency becomes a node-gyp source build wherever no prebuild matches the platform
|
|
80
|
+
* (musl/Alpine, a newer Node ABI, some ARM builds) and fails the install for every user of the CLI,
|
|
81
|
+
* including the majority who never touch delegation. `node:sqlite` is in the Node standard library at
|
|
82
|
+
* this package's own engine floor (`node >= 22`), needs no build step, and gives the identical
|
|
83
|
+
* primitives: `BEGIN IMMEDIATE` transactions, UNIQUE/CHECK constraints, conditional UPDATE with a
|
|
84
|
+
* `changes` count, WAL and `synchronous = FULL`.
|
|
85
|
+
*
|
|
86
|
+
* It is imported LAZILY and the failure is named: a runtime without `node:sqlite` opens an UNAVAILABLE
|
|
87
|
+
* store, and every delegated send then refuses by name rather than falling back to anything.
|
|
88
|
+
*
|
|
89
|
+
* 🔴 IT FAILS CLOSED, LOUDLY. No durable path configured, a database that cannot be opened, or a record
|
|
90
|
+
* that cannot be parsed means every delegated send refuses. An unreadable store is NOT an empty one, and
|
|
91
|
+
* the difference decides whether a person's cap is honoured.
|
|
92
|
+
*
|
|
93
|
+
* Nothing here logs a token, an address or a credential. A record is a user id, a mint key, two instants,
|
|
94
|
+
* a number, a version and a state.
|
|
95
|
+
*/
|
|
96
|
+
/** The explicit path override; otherwise the mounted Railway volume is used. */
|
|
97
|
+
export declare const DELEGATION_CAP_STORE_PATH_ENV: "OTTO_DELEGATION_CAP_STORE_PATH";
|
|
98
|
+
/** The database file inside that directory. The directory is kept so a legacy import can still read it. */
|
|
99
|
+
export declare const CAP_STORE_DB_FILE: "caps.db";
|
|
100
|
+
/**
|
|
101
|
+
* 🔴 THE STAGED-DEPLOY GATE (gate finding, CRITICAL 2 — the first rolling deployment).
|
|
102
|
+
*
|
|
103
|
+
* "The ruleset handshake cannot fence the first rolling deployment. The version refusal exists only in
|
|
104
|
+
* the new binary … status/reserve/confirm may hit a new worker while prepare/submit hits a still-serving
|
|
105
|
+
* old worker, which cannot interpret `requiredVersion` and executes anyway."
|
|
106
|
+
*
|
|
107
|
+
* The handshake cannot fix that, because the binary that must refuse is the one that has never heard of
|
|
108
|
+
* it. A DEPLOYMENT SEQUENCE can, and this flag is what makes the sequence possible:
|
|
109
|
+
*
|
|
110
|
+
* 1. deploy with `DELEGATION_MINT_ENABLED` unset or false — no new permission can be created at all,
|
|
111
|
+
* so there is no cap-bearing grant for an old worker to execute at the ceiling;
|
|
112
|
+
* 2. confirm exactly one cap-aware worker is serving (`/health` reports `rulesetVersion`);
|
|
113
|
+
* 3. set the flag true and redeploy.
|
|
114
|
+
*
|
|
115
|
+
* DEFAULT FALSE IN CODE. An operator who forgets the variable gets the paused surface, never an
|
|
116
|
+
* unfenced first rollout. Sends under permissions that are ALREADY confirmed keep working under their
|
|
117
|
+
* own cap, and REVOKE always works — pausing the creation of authority must never pause its withdrawal.
|
|
118
|
+
*/
|
|
119
|
+
export declare const DELEGATION_MINT_ENABLED_ENV: "DELEGATION_MINT_ENABLED";
|
|
120
|
+
/** True only for an explicit affirmative value. Absent, empty, or anything else is FALSE. */
|
|
121
|
+
export declare function resolveDelegationMintEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
122
|
+
/** The one sentence every paused surface says. No security narration: it is an update, and it is short. */
|
|
123
|
+
export declare const DELEGATION_MINT_PAUSED_MESSAGE: "Delegation is paused for a few minutes while we update it";
|
|
124
|
+
/**
|
|
125
|
+
* The shape a mint key must have: what `createDelegation({ idempotencyKey })` accepts, bounded so it
|
|
126
|
+
* cannot be used as a path or a smuggling channel. A UUID satisfies it.
|
|
127
|
+
*/
|
|
128
|
+
export declare const MINT_KEY_PATTERN: RegExp;
|
|
129
|
+
/** A CDP end-user id, as every schema on this surface bounds it. */
|
|
130
|
+
export declare const END_USER_ID_PATTERN: RegExp;
|
|
131
|
+
/**
|
|
132
|
+
* How far back the offset ladder may walk, in whole seconds, before it refuses.
|
|
133
|
+
*
|
|
134
|
+
* Five minutes of headroom on a grant measured in days: invisible to the reader, far more collisions
|
|
135
|
+
* than a real person can produce, and bounded so a corrupt or adversarially stuffed table cannot turn a
|
|
136
|
+
* request into one that never returns.
|
|
137
|
+
*/
|
|
138
|
+
export declare const CAP_EXPIRY_MAX_OFFSET_STEPS: 300;
|
|
139
|
+
/**
|
|
140
|
+
* 🔴 A RESERVATION THAT IS NEVER CONFIRMED DIES (seat ruling, round 4). Ten minutes is long enough for a
|
|
141
|
+
* person to finish an OTP sign-in and a mint, and short enough that an abandoned reservation stops being
|
|
142
|
+
* something a later actor could try to confirm. Its SECOND stays taken — releasing it would let a
|
|
143
|
+
* different permission land on an instant this one may already have minted at.
|
|
144
|
+
*/
|
|
145
|
+
export declare const CAP_RESERVATION_WINDOW_MS: number;
|
|
146
|
+
/**
|
|
147
|
+
* The version of the CAP RULESET this binary implements — distinct from the fence policy's version,
|
|
148
|
+
* which describes what Coinbase enforces.
|
|
149
|
+
*
|
|
150
|
+
* 🔴 WHY IT EXISTS (gate finding, HIGH — rolling deploy). "An older worker that accepts the same fence
|
|
151
|
+
* but lacks per-user cap enforcement can execute a new $10 permission up to the engine ceiling." The
|
|
152
|
+
* fence is identical on both binaries, so the fence cannot tell them apart. Every record carries the
|
|
153
|
+
* ruleset version it REQUIRES, a worker refuses a record it is too old to honour, and the page and the
|
|
154
|
+
* CLI refuse to reserve or mint against a server that reports a version below this one.
|
|
155
|
+
*
|
|
156
|
+
* 🔴 AND IT IS DEFENCE IN DEPTH, NOT THE FENCE FOR THE FIRST ROLLOUT — that is
|
|
157
|
+
* `DELEGATION_MINT_ENABLED` above, because a binary that predates the handshake cannot honour it.
|
|
158
|
+
*/
|
|
159
|
+
/** v2 (fence v2, 2026-09-03): a permission minted under v2 consents to Yield Copilot vault WITHDRAWS as well as swaps/deposits. */
|
|
160
|
+
export declare const MODEL_B_CAP_RULESET_VERSION: 2;
|
|
161
|
+
/** The lifecycle of one permission, as the gate prescribed it, plus the one state a MIGRATION writes. */
|
|
162
|
+
export declare const CAP_RECORD_STATES: readonly ["reserved", "confirmed_active", "superseded", "revoked", "expired", "legacy"];
|
|
163
|
+
export type CapRecordState = (typeof CAP_RECORD_STATES)[number];
|
|
164
|
+
/** One durable record. A row in `reservations`; `active_grants` points at the live one. */
|
|
165
|
+
export interface DelegationCapRecord {
|
|
166
|
+
readonly userId: string;
|
|
167
|
+
/** The `idempotencyKey` the permission is minted with. Names the reservation; authorises nothing. */
|
|
168
|
+
readonly mintKey: string;
|
|
169
|
+
/** The WHOLE-SECOND instant CDP will echo — the value every enforcement read compares against. */
|
|
170
|
+
readonly expiresAt: string;
|
|
171
|
+
/** What goes on the wire: the same second with milliseconds in 1..999, because CDP refuses `.000`. */
|
|
172
|
+
readonly requestedExpiresAt: string;
|
|
173
|
+
readonly perSwapCapUsd: number;
|
|
174
|
+
/** The cap-ruleset version a worker must implement to honour this record. */
|
|
175
|
+
readonly requiredVersion: number;
|
|
176
|
+
/**
|
|
177
|
+
* 🔴 THE REVOKE GENERATION THIS RESERVATION WAS MADE UNDER (gate finding, r4: "Add a durable revoke
|
|
178
|
+
* generation so Stop cancels already-reserved/in-flight mints without permanently poisoning later
|
|
179
|
+
* reservations"). Every Stop bumps the person's generation; a confirm whose reservation carries an
|
|
180
|
+
* older one is CANCELLED rather than promoted. That is how pressing Stop while a mint is in flight
|
|
181
|
+
* actually stops it — the old design read no grant yet, marked nothing, and the mint landed anyway.
|
|
182
|
+
*
|
|
183
|
+
* It poisons nothing: a reservation made AFTER the Stop carries the new generation and confirms
|
|
184
|
+
* normally, which is exactly what a per-user sticky marker could not do.
|
|
185
|
+
*/
|
|
186
|
+
readonly revokeGeneration: number;
|
|
187
|
+
readonly state: CapRecordState;
|
|
188
|
+
readonly reservedAt: string;
|
|
189
|
+
readonly confirmedAt?: string;
|
|
190
|
+
readonly endedAt?: string;
|
|
191
|
+
}
|
|
192
|
+
export interface DelegationCapReservation {
|
|
193
|
+
readonly outcome: 'reserved' | 'already_reserved';
|
|
194
|
+
readonly record: DelegationCapRecord;
|
|
195
|
+
}
|
|
196
|
+
export type DelegationCapConfirm = {
|
|
197
|
+
readonly outcome: 'confirmed';
|
|
198
|
+
readonly record: DelegationCapRecord;
|
|
199
|
+
}
|
|
200
|
+
/** Already confirmed and still the live one: a retry after a lost response. */
|
|
201
|
+
| {
|
|
202
|
+
readonly outcome: 'already_confirmed';
|
|
203
|
+
readonly record: DelegationCapRecord;
|
|
204
|
+
}
|
|
205
|
+
/** Nothing was reserved for this mint key. */
|
|
206
|
+
| {
|
|
207
|
+
readonly outcome: 'unreserved';
|
|
208
|
+
}
|
|
209
|
+
/** The reservation exists but its ten minutes passed, or it was imported, or it was already ended. */
|
|
210
|
+
| {
|
|
211
|
+
readonly outcome: 'not_reservable';
|
|
212
|
+
readonly state: CapRecordState;
|
|
213
|
+
}
|
|
214
|
+
/** It WAS confirmed and a later confirmation replaced it — never revived. */
|
|
215
|
+
| {
|
|
216
|
+
readonly outcome: 'superseded';
|
|
217
|
+
}
|
|
218
|
+
/** The person pressed Stop while this permission was being created. Never promoted. */
|
|
219
|
+
| {
|
|
220
|
+
readonly outcome: 'cancelled';
|
|
221
|
+
}
|
|
222
|
+
/** The live grant's instant is not the reserved one. */
|
|
223
|
+
| {
|
|
224
|
+
readonly outcome: 'expiry_mismatch';
|
|
225
|
+
readonly record: DelegationCapRecord;
|
|
226
|
+
};
|
|
227
|
+
/** What one official Stop did, durably: the generation now in force and the record it took away. */
|
|
228
|
+
export interface DelegationCapStop {
|
|
229
|
+
readonly generation: number;
|
|
230
|
+
/** The instant of the record this Stop moved to `revoked`; absent when nothing was confirmed-active. */
|
|
231
|
+
readonly revokedExpiresAt?: string;
|
|
232
|
+
}
|
|
233
|
+
export declare class DelegationCapExpiryUnavailableError extends Error {
|
|
234
|
+
constructor(steps: number);
|
|
235
|
+
}
|
|
236
|
+
export interface DelegationCapStore {
|
|
237
|
+
/** False when no durable path is configured, or the database could not be opened. Sends refuse while false. */
|
|
238
|
+
readonly available: boolean;
|
|
239
|
+
readonly unavailableReason?: string;
|
|
240
|
+
/**
|
|
241
|
+
* Reserve the first whole second at or BEFORE `latestExpiresAt` that no reservation for this user
|
|
242
|
+
* occupies, and record the cap against it. Idempotent per mint key. THROWS when the store cannot be
|
|
243
|
+
* written; `DelegationCapExpiryUnavailableError` when the ladder is exhausted.
|
|
244
|
+
*/
|
|
245
|
+
reserve(userId: string, mintKey: string, latestExpiresAt: string, perSwapCapUsd: number, nowMs: number): Promise<DelegationCapReservation>;
|
|
246
|
+
/** The reservation this mint key holds, with its state as of `nowMs` (the window applied). */
|
|
247
|
+
readReservation(userId: string, mintKey: string, nowMs: number): Promise<DelegationCapRecord | undefined>;
|
|
248
|
+
/**
|
|
249
|
+
* `reserved → confirmed_active`, ONE-SHOT, in a single transaction that also supersedes whatever was
|
|
250
|
+
* confirmed-active for this user before. A concurrent second confirm loses the conditional UPDATE and
|
|
251
|
+
* is answered `superseded` or `already_confirmed` — never a second winner.
|
|
252
|
+
*/
|
|
253
|
+
confirm(userId: string, mintKey: string, grantExpiresAt: string, nowMs: number): Promise<DelegationCapConfirm>;
|
|
254
|
+
/**
|
|
255
|
+
* 🔴 THE SEND GATE'S ONLY READ. The confirmed-active record for this user, or undefined. A reserved,
|
|
256
|
+
* superseded, revoked, expired or legacy record is NEVER returned here — that is what makes a
|
|
257
|
+
* reservation unable to authorise anything. THROWS when the store cannot be read.
|
|
258
|
+
*/
|
|
259
|
+
readActive(userId: string, nowMs: number): Promise<DelegationCapRecord | undefined>;
|
|
260
|
+
/**
|
|
261
|
+
* Mark the confirmed-active record for this user REVOKED, but only when it is the grant at
|
|
262
|
+
* `expiresAt`. Per-grant by construction: a revoke aimed at a permission that is no longer the live
|
|
263
|
+
* one changes nothing, which is what stops a stale tab taking away a permission it never held.
|
|
264
|
+
*
|
|
265
|
+
* 🔴 THE REVOKED ROW STAYS, AND ITS SECOND STAYS CONSUMED. That is the "consumed-expiry state" the
|
|
266
|
+
* gate asked for: after a revoke, no later grant can be minted at that instant through Otto's page or
|
|
267
|
+
* CLI (the ladder skips it), and one minted there out-of-model matches no confirmed record, so it
|
|
268
|
+
* authorises nothing.
|
|
269
|
+
*/
|
|
270
|
+
revoke(userId: string, expiresAt: string, nowMs: number): Promise<boolean>;
|
|
271
|
+
/**
|
|
272
|
+
* 🔴 STOP CANCELS WHAT IS ALREADY IN FLIGHT. Bump this person's revoke generation, so any reservation
|
|
273
|
+
* made before now can never be confirmed. It is not a poison: it is a monotonic counter, and the next
|
|
274
|
+
* reservation carries the new value. Returns the generation now in force.
|
|
275
|
+
*
|
|
276
|
+
* ⚠️ NOT THE OFFICIAL STOP. Every official revoke goes through `stop` below, which does this AND the
|
|
277
|
+
* record transition in ONE transaction. This stays on the interface for the one-shot cancel case a
|
|
278
|
+
* test drives directly; nothing in the admin path calls it on its own any more.
|
|
279
|
+
*/
|
|
280
|
+
bumpRevokeGeneration(userId: string, nowMs: number): Promise<number>;
|
|
281
|
+
/**
|
|
282
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
283
|
+
* 🔴 THE OFFICIAL STOP — ONE TRANSACTION, TWO WRITES, NEVER ONE WITHOUT THE OTHER (gate r5 CRITICAL)
|
|
284
|
+
* ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
285
|
+
* "`revokeDelegation` commits the generation bump and then performs the active-record revoke in a
|
|
286
|
+
* separate transaction … The process dies or stalls before `capStore.revoke()` … A fresh worker still
|
|
287
|
+
* reads the old record as `confirmed_active`; the unchanged live grant matches and a send remains
|
|
288
|
+
* admissible."
|
|
289
|
+
*
|
|
290
|
+
* So the Stop is one `BEGIN IMMEDIATE` transaction that (1) bumps the revoke generation — cancelling
|
|
291
|
+
* every reservation taken before now, including a mint already in flight with no grant to find yet —
|
|
292
|
+
* and (2) moves whatever record is confirmed-active for this person to `revoked`. A crash between the
|
|
293
|
+
* two is a crash INSIDE the transaction: nothing is committed, the caller is told to retry, and nothing
|
|
294
|
+
* is revoked at Coinbase either. The revoked row survives and holds its second (the consumed expiry).
|
|
295
|
+
*
|
|
296
|
+
* It takes NO expiry, deliberately. A Stop means "Otto acts under nothing for me": the record that is
|
|
297
|
+
* live goes, whatever tab pressed the button and whatever grant Coinbase happens to hold — exactly what
|
|
298
|
+
* the CDP-side revoke that follows it does. `revoke` above stays per grant for the mismatch path, where
|
|
299
|
+
* the thing being taken away IS one specific grant.
|
|
300
|
+
*
|
|
301
|
+
* Answers the generation now in force and the instant of the record it revoked, if any.
|
|
302
|
+
*/
|
|
303
|
+
stop(userId: string, nowMs: number): Promise<DelegationCapStop>;
|
|
304
|
+
/** Release the database handle. Present on the real store; tests call it, production never needs to. */
|
|
305
|
+
close?(): void;
|
|
306
|
+
}
|
|
307
|
+
export declare function resolveDelegationCapStorePath(env?: NodeJS.ProcessEnv): string | null;
|
|
308
|
+
/** The instant `ms` floored to a whole second — the only granularity CDP preserves. */
|
|
309
|
+
export declare function wholeSecondMs(ms: number): number;
|
|
310
|
+
/**
|
|
311
|
+
* The value that goes ON THE WIRE for a reserved second.
|
|
312
|
+
*
|
|
313
|
+
* 🔴 CDP REFUSES a `.000` fraction — 401 `unauthorized` "Wallet authentication error", a code that says
|
|
314
|
+
* nothing about the cause (isolated live on one session, 2026-09-02: `.000Z` refused, `.137Z` accepted
|
|
315
|
+
* on the very next call, `.000Z` refused again). And it TRUNCATES the fraction when it stores the grant
|
|
316
|
+
* (`…:46.001Z` → `…:46Z`, `…:45.999Z` → `…:45Z`), so any millisecond in 1..999 lands on the reserved
|
|
317
|
+
* second and none of them lengthens the permission.
|
|
318
|
+
*/
|
|
319
|
+
export declare const MINT_EXPIRY_FILLER_MS: 1;
|
|
320
|
+
export declare function mintExpiryForSecond(wholeSecondIso: string): string;
|
|
321
|
+
/**
|
|
322
|
+
* The offset ladder as a pure function: the first whole second at or BEFORE `latestMs` that `taken`
|
|
323
|
+
* does not hold and that is still in the future, or undefined. It NEVER returns anything later than
|
|
324
|
+
* `latestMs` — a permission must not outlive the duration the person chose, not even by a second.
|
|
325
|
+
*/
|
|
326
|
+
export declare function firstFreeExpiryMs(latestMs: number, nowMs: number, taken: (candidateMs: number) => boolean, maxSteps?: number): number | undefined;
|
|
327
|
+
/** A record's state as of `nowMs`: a reservation whose window has passed reads as `expired`. */
|
|
328
|
+
export declare function stateAsOf(record: DelegationCapRecord, nowMs: number): CapRecordState;
|
|
329
|
+
/**
|
|
330
|
+
* The two pragmas the durability claim rests on, read back FROM THE CONNECTION THAT SET THEM.
|
|
331
|
+
*
|
|
332
|
+
* 🔴 `synchronous` IS PER CONNECTION. A test that opens its own handle to the same file always reads the
|
|
333
|
+
* default, so an assertion made that way passes whatever this store did — the mutation that dropped
|
|
334
|
+
* `synchronous` to OFF survived exactly one run because of it. `journal_mode` is persisted in the file;
|
|
335
|
+
* this one is not, so the store has to say what it applied.
|
|
336
|
+
*/
|
|
337
|
+
export interface AppliedPragmas {
|
|
338
|
+
readonly journalMode: string;
|
|
339
|
+
readonly synchronous: number;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Everything a pre-SQLite store left in `root`: the round-1/2 append-only JSONL, and the round-4
|
|
343
|
+
* per-instant JSON files under `u/<userId>/r/`. Read best-effort — a migration that THREW on an
|
|
344
|
+
* unreadable leftover would stop the store opening at all, and the leftovers authorise nothing either
|
|
345
|
+
* way.
|
|
346
|
+
*/
|
|
347
|
+
export declare function readLegacyCapRecords(root: string): Promise<DelegationCapRecord[]>;
|
|
348
|
+
/** A store that is present but permanently unavailable — what an unconfigured deploy gets. */
|
|
349
|
+
export declare function unavailableDelegationCapStore(reason: string): DelegationCapStore;
|
|
350
|
+
/**
|
|
351
|
+
* Open the SQLite store at `root`, importing anything a pre-SQLite store left behind as `legacy`.
|
|
352
|
+
* Exported so a test can open one on a temp directory without going through the environment.
|
|
353
|
+
*/
|
|
354
|
+
export declare function openDelegationCapStore(root: string): Promise<DelegationCapStore>;
|
|
355
|
+
/**
|
|
356
|
+
* The store for this process, from the environment. Unconfigured is NOT a silent in-memory fallback: it
|
|
357
|
+
* is an unavailable store, so the delegated send refuses by name and the ops fix (mount the volume, or
|
|
358
|
+
* set `OTTO_DELEGATION_CAP_STORE_PATH`) is the thing the refusal asks for.
|
|
359
|
+
*/
|
|
360
|
+
export declare function createDelegationCapStore(env?: NodeJS.ProcessEnv): Promise<DelegationCapStore>;
|
|
361
|
+
/**
|
|
362
|
+
* An in-memory store — for TESTS and for a local stdio run, where nothing is durable anyway.
|
|
363
|
+
*
|
|
364
|
+
* 🔴 IT IMPLEMENTS THE SAME STATE MACHINE, and the suite runs the SAME conformance cases against both,
|
|
365
|
+
* so a test cannot pass against semantics production does not have. What it cannot model is the
|
|
366
|
+
* cross-process exclusion and the crash durability SQLite provides — those are asserted against a real
|
|
367
|
+
* database file, and against two real processes, only.
|
|
368
|
+
*/
|
|
369
|
+
export declare function inMemoryDelegationCapStore(seed?: readonly DelegationCapRecord[]): DelegationCapStore;
|