cascrow-signer 0.1.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,6 +1,71 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 — unreleased (owner publishes)
3
+ ## 0.3.0 — 2026-10-21
4
+
5
+ - **`object` commits to a salted hash of the reason.** What goes on chain is now
6
+ `keccak256(salt ‖ utf8(reason))` (= `solidityPackedKeccak256(["bytes32","string"], [salt, reason])`),
7
+ with a salt of 32 random bytes made on this machine; 0.2.0 sent `keccak256(utf8(reason))`,
8
+ and that hash stands on two public ledgers for good (the `Objected` event, and cascrow's
9
+ audit memo on the XRP Ledger), where a short reason could be confirmed by hashing guesses.
10
+ The result of `object` carries `reason`, `reasonSalt` and `reasonHash`, and the spending
11
+ journal keeps the same three on the `object` reservation (`objection`). Keep the salt with
12
+ the reason: without it nobody, you included, can show later which reason the hash commits
13
+ to. Neither the reason nor the salt is sent to cascrow — `prepare` still gets only the hash.
14
+ The contract does not read the hash; nothing else changes. Every `cascrow-signer` pin is now
15
+ `cascrow-signer@0.3.0`.
16
+
17
+ ## 0.2.0 — 2026-10-19
18
+
19
+ - **Upgrade required to link a wallet** (cascrow SEC-145). cascrow's wallet challenge is now a
20
+ Sign-In with Ethereum (EIP-4361) message that names cascrow's host, the address being linked
21
+ and the chain; the old one-line sentence named none of them. `link` rebuilds the message from
22
+ the challenge token's fields and signs only on an exact match with the configured
23
+ `CASCROW_BASE_URL` host, this key's address, the pinned chain ID and an unexpired time
24
+ (`CHALLENGE_MISMATCH` / `CHALLENGE_EXPIRED` otherwise — nothing is signed). 0.1.0's `link`
25
+ asks for the challenge without the address it now has to name, and cascrow answers that with
26
+ HTTP 400 `CHALLENGE_ADDRESS_REQUIRED` ("address is required … update to 0.2.0"), so 0.1.0 never
27
+ gets a message to sign — the safe failure; every other command of 0.1.0 keeps working.
28
+ - `link --old-home DIR` (API: `link({ oldHome, oldPassphrase })`): replacing a wallet already
29
+ linked to the account signs cascrow's `wallet-repoint` challenge with the OLD key in that
30
+ keystore — checked to be the linked wallet — because cascrow re-points an API-key account only
31
+ with the current wallet's consent. Without it, a known re-point is refused up front with
32
+ `REPOINT_NEEDS_OLD_KEY`, before anything is signed.
33
+ - API: `CHALLENGE_PREFIX` is gone (there is no fixed sentence any more); `checkChallenge(message,
34
+ token, expectation)` takes what the message must name, and `buildChallengeMessage` /
35
+ `challengeStatement` / `CHALLENGE_AUDIENCE` are exported.
36
+ - **The spending journal is integrity-protected** (cascrow SEC-149). Every line carries a MAC
37
+ chained to the line before it, under a key derived from the keystore key (the one the policy
38
+ and the escrow book already use). 0.1.0 obeyed whatever the file said: a hand-written
39
+ `release` line freed the 24-hour limit, an edited amount hid a funding, a forged
40
+ `signed`/`unknown` pair made the signer release a reservation, and `speed-up` re-signed the
41
+ `data`, `value` and `gasLimit` of the line it found. Now `fund` refuses a journal whose chain
42
+ breaks (`JOURNAL_TAMPERED`, with the way out); `speed-up` re-signs only an authentic entry
43
+ that moves no value (`SPEEDUP_REFUSED` for a funding), after decoding its calldata and
44
+ re-running the same checks against the chain and the policy; and the journal never holds a
45
+ signature. A 0.1.0 journal is migrated on the first transaction: its reservations still
46
+ count, its releases are ignored. The day sum is, per escrow, the larger of the journal's
47
+ reservation and the chain's `Funded` event, and a journal that cannot stand alone for the
48
+ last 24 hours (missing, 0.1.0, recreated less than a day ago) refuses `fund` when the chain
49
+ cannot be read. `object` and every other non-funding action go ahead over a damaged journal,
50
+ with a warning. A garbage line no longer blinds `watch` / `claims`
51
+ (`JOURNAL_UNREADABLE` there is gone — the watcher reads the journal only as hints).
52
+ - **A maximum deadline in the policy** (SEC-150): `fund` and `extend-deadline` refuse a
53
+ deadline more than `--max-deadline-days` away (default 365; the contract allows five years).
54
+ On a terminal, `fund` and `accept` print the terms and ask y/N before signing (`DECLINED`).
55
+ - **`return-funds` through MCP needs an opt-in** (SEC-151): off a terminal it is refused
56
+ (`RETURN_FUNDS_NEEDS_TERMINAL`) unless the policy was set with `--allow-agent-return-funds`;
57
+ on a terminal it asks first.
58
+ - **`watch` keeps a cursor and says when it is blind** (SEC-152): Funded events are read from
59
+ the last block read (with a 64-block reorg overlap) instead of from the deploy block every
60
+ round; after `--blind-after` failed rounds in a row (default 3) a `watch.blind` alarm goes
61
+ through `--exec` and the webhook, and `watch.recovered` when it reads again. The `--exec`
62
+ command now also gets `CASCROW_ALARM` (`claim.open`, `watch.blind`, `watch.recovered`).
63
+ `watch --once` exits non-zero (`WATCH_BLIND`) when it could not read the chain. A failed
64
+ alarm delivery is reported on stderr.
65
+ - `init` refuses a home that still holds an earlier key's journal (`JOURNAL_EXISTS`).
66
+ - **`accept` names the free veto** when the offer has no arbiter (the signed arbiter is the zero address): the terminal summary shows a `WARNING:` line before the y/N — the payer can object to every claim at no cost, and an objected claim ends in a refund after the deadline, so the escrow protects you only against an inactive payer — and the result's `note` carries the same sentence for an MCP host. The words are cascrow's (`V3_CLIENT_TEXT.noArbiterVeto`), held equal by a test.
67
+
68
+ ## 0.1.0 — 2026-09-27
4
69
 
5
70
  - First version. A local signer for cascrow's MilestoneEscrowV3 escrows: `init` (key and
6
71
  encrypted keystore on this machine), `policy` (limits in XRP, counterparty and arbiter
@@ -8,9 +73,10 @@
8
73
  `fund`, `submit-claim`, `claim`, `release`, `object`, `refund`, `extend-deadline`,
9
74
  `return-funds`, `withdraw`, `claims`, `watch`, `speed-up`, and an MCP server (`mcp`)
10
75
  with the `cascrow_signer_*` tools.
11
- - Chain, public RPC, contract address and runtime code hash are pinned in the package; no
12
- deployment is pinned yet, so every network is refused until the contract is deployed
13
- (testnet accepts an explicit, announced override).
76
+ - Chain, public RPC, contract address and runtime code hash are pinned in the package:
77
+ testnet (1449000) is `0xa1f10a285d9de4cb1ff613829e3a26222db65f45`, deployed in block
78
+ 8816108; mainnet has no deployment and is refused (testnet accepts an explicit,
79
+ announced override).
14
80
  - One runtime dependency, `ethers` 6.17.0, pinned exactly, with `npm-shrinkwrap.json`.
15
81
  - `submit-claim` takes `--proof-hash` (MCP: `proofHash`) — the `manifestHash` the evidence
16
82
  door returned — and signs only after cascrow confirms it is a sealed submission of the
package/MIGRATION.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Migrating to cascrow-signer
2
2
 
3
+ ## From 0.2.0 to 0.3.0
4
+
5
+ Nothing to do: the keystore, the escrow book, the policy and the journal are compatible both
6
+ ways (0.2.0 reads a 0.3.0 journal and ignores the new field). What changes is `object`: the
7
+ hash it puts on chain is salted — `keccak256(salt ‖ utf8(reason))` — and its result returns
8
+ `reasonSalt`. Keep the salt with the reason (the journal keeps a copy); an objection made with
9
+ 0.2.0 committed to the unsalted `keccak256(utf8(reason))`, which stays as it is on chain.
10
+
11
+ ## From 0.1.0 to 0.2.0
12
+
13
+ cascrow's wallet challenge changed to a Sign-In with Ethereum (EIP-4361) message (SEC-145)
14
+ that names the wallet being linked, so the challenge request has to carry that address. 0.1.0
15
+ asks without it, and `link` fails with
16
+ `address is required: … update to 0.3.0 (npx cascrow-signer@0.3.0 link) … [CHALLENGE_ADDRESS_REQUIRED]`
17
+ (HTTP 400) before anything is signed — safe, but `link` no longer works until you upgrade:
18
+ `npx cascrow-signer@0.3.0 link`. The keystore, the escrow book and your policy file are
19
+ compatible; what else changes:
20
+
21
+ - **The spending journal** (`journal.jsonl`) is upgraded to a MAC-chained format on the first
22
+ transaction 0.2.0 signs — nothing to do. Its existing reservations keep counting toward the
23
+ 24-hour limit; its `release` lines are ignored, so a funding 0.1.0 had released may count
24
+ for up to a day longer. For that first day, `fund` needs the chain's `Funded` events to be
25
+ readable. Do not edit the journal by hand: `fund` refuses one that was changed
26
+ (`JOURNAL_TAMPERED`). If that happens and you did not do it, check the machine; to go on,
27
+ move the file aside — the next `fund` rebuilds today's total from the chain.
28
+ - **Your policy** gains two fields with safe defaults: a maximum deadline of 365 days for
29
+ `fund` and `extend-deadline` (raise it with `policy set … --max-deadline-days N`, up to the
30
+ contract's five years), and `return-funds` through MCP is off (`--allow-agent-return-funds`
31
+ turns it on). `policy set` replaces the whole policy, so repeat every flag you want to keep.
32
+ - **On a terminal**, `fund`, `accept` and `return-funds` show the terms and ask y/N. Scripts
33
+ that ran them with a TTY attached now wait for an answer; run them through the MCP server
34
+ or without a terminal if no person is there.
35
+ - **`speed-up`** replaces only transactions that move no value; a stuck `fund` is never
36
+ replaced (`SPEEDUP_REFUSED`) — it keeps counting toward the limit until its nonce is used.
37
+ An entry from a 0.1.0 journal is not sped up.
38
+ - **`watch`**: the `--exec` command gets `CASCROW_ALARM` (`claim.open`, `watch.blind`,
39
+ `watch.recovered`) — check it in your script if it should only page on claims. `--once`
40
+ exits non-zero when it could not read the chain.
41
+
42
+ Replacing a wallet that is already linked now needs the current wallet's consent when you use
43
+ an API key: `cascrow-signer link --old-home <directory of the old keystore>`. If the old key is
44
+ lost, change the wallet from a signed-in cascrow session (password or 2FA), or contact support.
45
+
3
46
  ## From `cascrow_escrow_fund` / `escrowFund()` / `cascrow escrow-fund --private-key`
4
47
 
5
48
  Those paths sent your private key to cascrow, which signed the deposit on its servers.
@@ -11,7 +54,7 @@ do not link the old address again (cascrow refuses it anyway).
11
54
  |---|---|
12
55
  | `cascrow_escrow_fund { agentPrivateKey, … }` | `cascrow_signer_fund { milestoneId }` in this package's MCP server |
13
56
  | `client.escrowFund({ privateKey, … })` | `createSigner(configFromEnv(process.env, io)).fund({ milestoneId })` |
14
- | `cascrow escrow-fund --private-key …` | `npx cascrow-signer@0.1.0 fund --milestone <id>` |
57
+ | `cascrow escrow-fund --private-key …` | `npx cascrow-signer@0.3.0 fund --milestone <id>` |
15
58
  | the wallet cascrow generated at registration | `cascrow-signer init` + `cascrow-signer link` |
16
59
 
17
60
  ## v2 (RLUSD) escrows
package/README.md CHANGED
@@ -15,23 +15,36 @@ it signs against what it knows independently of cascrow.
15
15
  Install a pinned version — never `@latest` for a tool that holds money:
16
16
 
17
17
  ```sh
18
- npx cascrow-signer@0.1.0 help
18
+ npx cascrow-signer@0.3.0 help
19
19
  ```
20
20
 
21
- > **Status: unreleased.** The MilestoneEscrowV3 contract is not deployed yet, so every
22
- > network is refused until a version with a pinned deployment ships. On testnet a
23
- > deployment can be named explicitly (see *Testnet override*).
21
+ > **Status: released (0.1.0, on npm since 2026-09-27).** MilestoneEscrowV3 is deployed on
22
+ > the XRPL EVM testnet (chain 1449000) and pinned in this package. Mainnet has no
23
+ > deployment yet, so mainnet stays refused until a version with a pinned mainnet
24
+ > deployment ships. On testnet a deployment can also be named explicitly (see
25
+ > *Testnet override*).
24
26
 
25
27
  ## Setup
26
28
 
27
29
  ```sh
28
30
  export CASCROW_SIGNER_NETWORK=testnet # or mainnet — never guessed
29
31
  cascrow-signer init # new key, encrypted keystore in ~/.cascrow/signer
30
- cascrow-signer policy set --per-escrow-xrp 500 --daily-xrp 2000 --min-review-hours 72
32
+ cascrow-signer policy set --per-escrow-xrp 500 --daily-xrp 2000 --min-review-hours 72 --max-deadline-days 365
31
33
  export CASCROW_API_KEY=csk_… # your cascrow account
32
34
  cascrow-signer link # proves this address is yours to cascrow
33
35
  ```
34
36
 
37
+ `link` signs cascrow's Sign-In with Ethereum (EIP-4361) challenge only after rebuilding it
38
+ from the challenge token: it must name the host of `CASCROW_BASE_URL`, this key's address and
39
+ the pinned chain, and must not have expired. To REPLACE a wallet already linked to the
40
+ account, cascrow wants the current wallet's signature too: keep its keystore and run
41
+ `cascrow-signer link --old-home <its directory>` on a terminal (or change the wallet from a
42
+ signed-in cascrow session, which asks for your password or 2FA).
43
+
44
+ `policy set` replaces the whole policy (limits in XRP, counterparty and arbiter lists, minimum
45
+ review period, maximum deadline — default 365 days — and `--allow-agent-return-funds`, off by
46
+ default). It works on a terminal only.
47
+
35
48
  `~/.cascrow/signer` is created `0700`, every file in it `0600`; a file other users can
36
49
  read is refused, not repaired. Set `CASCROW_SIGNER_HOME` to use another directory.
37
50
 
@@ -44,8 +57,8 @@ read. There is no OS-keychain integration on purpose (native dependency, supply
44
57
  ```sh
45
58
  cascrow-signer fund --milestone <id> # one transaction, value = amount, no approve
46
59
  cascrow-signer claims # live claims on your escrows, read from the chain
47
- cascrow-signer watch --exec ./page-me.sh # alarm for every claim whose window is open
48
- cascrow-signer object --escrow 0x… --reason "Criterion 2 is not met: …"
60
+ cascrow-signer watch --exec ./page-me.sh # alarm for every claim whose window is open — and when it is blind
61
+ cascrow-signer object --escrow 0x… --reason "Criterion 2 is not met: …" # result: reasonHash + reasonSalt — keep the salt
49
62
  cascrow-signer release --escrow 0x…
50
63
  cascrow-signer extend-deadline --escrow 0x… --new-deadline 1767225600
51
64
  ```
@@ -54,9 +67,18 @@ Before `fund` signs, it checks — against values it knows itself, not the serve
54
67
  the escrow ID is `escrowIdFor(your address, ref, salt)`; the token is native XRP; `value`
55
68
  equals the amount; the amount is within your per-escrow limit (a lower one for a payee not
56
69
  in your counterparty book) and your rolling 24-hour limit; the arbiter is absent or one you
57
- listed; the review period is at least your minimum; the deadline is within the contract's
58
- maximum and `acceptBy` still open **by chain time**; and the payee's signature verifies
59
- under the pinned chain and contract. The escrow is then written to your local book.
70
+ listed; the review period is at least your minimum; the deadline is within your policy's
71
+ maximum (default 365 days; the contract allows five years) and `acceptBy` still open **by
72
+ chain time**; and the payee's signature verifies under the pinned chain and contract. On a
73
+ terminal it then shows you the terms and asks before it signs. The escrow is then written
74
+ to your local book.
75
+
76
+ The 24-hour limit is kept in a local spending journal whose every line carries a MAC chained
77
+ to the one before it (a key derived from your private key, like the policy's). A journal that
78
+ was changed by anything but this signer is refused for `fund` (`JOURNAL_TAMPERED`, with the
79
+ way out: move it aside, and the next fund rebuilds today's total from the chain). It never
80
+ blocks an objection. A 0.1.0 journal is upgraded on the first transaction: its reservations
81
+ keep counting, its releases do not.
60
82
 
61
83
  After funding, `object`, `release`, `refund`, `extend-deadline`, `claim`, `withdraw` and
62
84
  `return-funds` are built from the escrow ID and the chain alone — **cascrow does not need
@@ -68,8 +90,18 @@ escrows in your local book whose on-chain payer is you. When cascrow is reachabl
68
90
  `COMPLIANCE_BLOCK`), the signer stops and shows that refusal instead of sending anyway;
69
91
  only an unreachable cascrow, a 5xx, a rate limit or a bad API key count as "not available".
70
92
 
93
+ `extend-deadline` never moves the deadline further than your policy's maximum from now.
94
+
71
95
  A pending objection that is not being mined: `cascrow-signer speed-up --tx <hash>`
72
- (same nonce, fee +25%). Never send the same action twice.
96
+ (same nonce, fee +25%). Only a transaction this signer recorded in its intact journal and
97
+ that moves no value is replaced — a funding never is — and it is checked against the chain
98
+ again first. Never send the same action twice.
99
+
100
+ `watch` keeps a cursor and reads only new blocks after its first round. When it cannot read
101
+ the chain for `--blind-after` rounds in a row (default 3) it raises a `watch.blind` alarm
102
+ through the same `--exec` command and webhook (the command sees `CASCROW_ALARM=watch.blind`;
103
+ a claim is `CASCROW_ALARM=claim.open`), and a `watch.recovered` one when it reads again.
104
+ `watch --once` exits non-zero when it could not read the chain.
73
105
 
74
106
  ## As payee
75
107
 
@@ -80,8 +112,8 @@ cascrow-signer return-funds --escrow 0x…
80
112
  ```
81
113
 
82
114
  `accept` refuses an offer whose payee is not your address, whose token is not native XRP,
83
- whose arbiter you have not listed, or whose `acceptBy` has passed. **Your acceptance binds
84
- you until `acceptBy`** — the payer can fund these exact terms until then, and nothing in
115
+ whose arbiter you have not listed, or whose `acceptBy` has passed; on a terminal it shows the
116
+ terms and asks first. **Your acceptance binds you until `acceptBy`** — the payer can fund these exact terms until then, and nothing in
85
117
  cascrow revokes it.
86
118
 
87
119
  `submit-claim` claims with ONE sealed submission. Every evidence upload (one request —
@@ -94,11 +126,15 @@ claim naming evidence cascrow never assessed has no recommendation and invites a
94
126
  objection. Claim with a submission whose verdict is YES (`claimReady`); with NO or PENDING
95
127
  the payer is likely to object.
96
128
 
129
+ `return-funds` gives the escrow back to the payer for good. On a terminal it asks first;
130
+ through the MCP server it is refused (`RETURN_FUNDS_NEEDS_TERMINAL`) unless you allowed it
131
+ on a terminal with `cascrow-signer policy set … --allow-agent-return-funds`.
132
+
97
133
  ## As an MCP server
98
134
 
99
135
  ```json
100
136
  { "mcpServers": { "cascrow-signer": {
101
- "command": "npx", "args": ["cascrow-signer@0.1.0", "mcp"],
137
+ "command": "npx", "args": ["cascrow-signer@0.3.0", "mcp"],
102
138
  "env": { "CASCROW_SIGNER_NETWORK": "testnet", "CASCROW_API_KEY": "csk_…",
103
139
  "CASCROW_KEYSTORE_PASSPHRASE_FILE": "/home/me/.cascrow/pass" } } } }
104
140
  ```
@@ -107,7 +143,9 @@ Tools: `cascrow_signer_status`, `_link`, `_accept`, `_fund`, `_submit_claim`, `_
107
143
  `_release`, `_extend_deadline`, `_claim`, `_refund`, `_return_funds`, `_withdraw`,
108
144
  `_onchain_claims`, `_speed_up`. They are all local; cascrow's hosted MCP server and the
109
145
  `cascrow-mcp` package offer none of them, and no tool takes a key or a passphrase. The
110
- policy cannot be changed through MCP — only on a terminal.
146
+ policy cannot be changed through MCP — only on a terminal. The MCP server never asks a
147
+ question: what bounds it is the policy (limits, maximum deadline, and `_return_funds` only
148
+ with the opt-in).
111
149
 
112
150
  Use it next to `cascrow-mcp` (or the hosted server), which carries the remote
113
151
  `cascrow_v3_*` tools: offers, acceptance data, prepared transactions, attestations.
package/SECURITY.md CHANGED
@@ -8,7 +8,7 @@ contract (rule I9). This package is where the key lives. The line between the tw
8
8
 
9
9
  | Leaves this machine for cascrow | Never leaves this machine |
10
10
  |---|---|
11
- | the wallet-link signature over cascrow's fixed challenge sentence | the private key |
11
+ | wallet-challenge signatures (link; on a re-point, the old wallet's consent) over a Sign-In with Ethereum message the signer rebuilt and checked | the private key |
12
12
  | the payee's acceptance signature (EIP-712, pinned domain) | the passphrase |
13
13
  | a transaction **hash**, as a hint to re-read the chain | the keystore file |
14
14
  | your API key, as the `Authorization` header | any signed transaction |
@@ -24,14 +24,30 @@ transaction.
24
24
 
25
25
  - **A wrong or compromised cascrow API.** Chain ID, contract address and runtime code hash
26
26
  come from the package, not from the server; every term of a `fund` is checked against
27
- your address, your policy, the chain's clock and the payee's signature under the pinned
27
+ your address, your policy (amount limits, counterparty book, arbiter list, minimum review
28
+ period, maximum deadline), the chain's clock and the payee's signature under the pinned
28
29
  domain; `data` is rebuilt from the ABI. A server can refuse to help you, but it cannot
29
30
  make this signer fund different terms, pay a different contract or sign a message that is
30
31
  not the wallet challenge.
31
32
  - **A confused or prompt-injected agent driving the MCP tools.** Limits per escrow and per
32
- 24 hours, a lower limit for unknown payees, the arbiter allowlist and the minimum review
33
- period are enforced locally; the policy can only be changed on a terminal, and its file
33
+ 24 hours, a lower limit for unknown payees, the arbiter allowlist, the minimum review
34
+ period and the maximum deadline (default 365 days, for `fund` and `extend-deadline`; the
35
+ contract itself allows five years) are enforced locally; `return-funds` — which gives the
36
+ escrow back for good — is refused through MCP unless you allowed it on a terminal
37
+ (`--allow-agent-return-funds`). The policy can only be changed on a terminal, and its file
34
38
  carries a MAC under a key derived from your private key.
39
+ - **A person at the terminal signing what they did not read.** On a terminal, `fund`,
40
+ `accept` and `return-funds` print the terms (amount, counterparty, deadline, review period,
41
+ arbiter) and ask y/N before anything is signed; a "no" signs nothing.
42
+ - **A process that can write the signer's directory but cannot unlock the key** — for the
43
+ spending journal too, not only the policy and the escrow book. Every journal line carries
44
+ a MAC chained to the line before it, under a key derived from your private key. A line
45
+ that was forged, edited, reordered or removed in the middle breaks the chain, and `fund`
46
+ is then refused (`JOURNAL_TAMPERED`) instead of computing your 24-hour limit over it;
47
+ `speed-up` re-signs only an authentic entry, only one that moves no value, and only after
48
+ decoding its calldata and checking the action against the chain again. The journal never
49
+ holds a signature or a signed transaction. Actions that do not spend from the limit
50
+ (`object` above all) are never blocked by a damaged journal; they say so in a warning.
35
51
  - **Losing cascrow.** Every action after funding but `submitClaim` works from the chain
36
52
  alone; the watcher reads your escrows from the chain. `object` needs neither cascrow nor
37
53
  the local book — only that the chain names this key as the payer. `submitClaim` needs
@@ -40,8 +56,15 @@ transaction.
40
56
  than max(review period, 72 h, 25 % of the remaining term) before the deadline: the payer
41
57
  can fund at any moment up to `acceptBy`, so what follows `acceptBy` is the payee's
42
58
  guaranteed time to deliver.
43
- - **A deleted journal.** Today's spending is rebuilt from `Funded(payer = you)` events; if
44
- neither is readable, `fund` is refused.
59
+ - **A deleted, cut or rolled-back journal.** Today's spending is, per escrow, the larger of
60
+ what the journal reserved and what the chain's `Funded(payer = you)` events of the last
61
+ 24 hours say. A journal that cannot stand alone for the whole window — missing, written by
62
+ 0.1.0, or recreated less than 24 hours ago — needs the chain: if the chain cannot be read,
63
+ `fund` is refused.
64
+ - **A blind watcher.** `watch` raises an alarm through your `--exec` command and webhook not
65
+ only for an open claim but also when it could not read the chain for `--blind-after`
66
+ rounds in a row (default 3), and again when it can read again; `watch --once` exits
67
+ non-zero when it could not read the chain.
45
68
 
46
69
  ## What it does NOT protect against
47
70
 
@@ -55,11 +78,15 @@ transaction.
55
78
  And until `acceptBy` the payer holds a free option to fund or let the offer lapse; the
56
79
  payee's minimum work window (above) bounds what that option can cost the payee.
57
80
  - **A process that can read your passphrase.** It can unlock the key, recompute the policy
58
- MAC and write any limits it likes. If an agent runs with `CASCROW_KEYSTORE_PASSPHRASE` in
81
+ and journal MACs and write any limits it likes. If an agent runs with `CASCROW_KEYSTORE_PASSPHRASE` in
59
82
  its environment or can read the passphrase file, the policy stops honest mistakes, not
60
83
  that agent. Run the signer as a separate user or on a separate machine if that matters.
61
84
  - **Malware on the machine**, a compromised Node.js, or a compromised `ethers` (pinned
62
85
  exactly and shrinkwrapped, so an upgrade is a visible change — not a guarantee).
86
+ - **The newest lines of the journal being cut off, or an older copy put back.** The MAC chain
87
+ cannot see that the tail is missing. A funding that is already on chain is still counted
88
+ (the chain's `Funded` events); a funding still in flight when its lines were cut is not,
89
+ so the 24-hour limit can be exceeded by at most the amounts in flight at that moment.
63
90
  - **A compromised release of this package.** Install a pinned version and check it.
64
91
  - **The public RPC lying about chain state** (it cannot forge your signature, but it could
65
92
  hide a claim from `watch`). Point `watch` at a second RPC you trust if the review window
@@ -53,6 +53,13 @@ export type AcceptInput = {
53
53
  milestoneId: string;
54
54
  minAmountXrp?: string;
55
55
  };
56
+ /**
57
+ * The free veto of an escrow without an arbiter (cascrow S-05, accepted on the
58
+ * condition that the payee knows before signing). The same words as cascrow's
59
+ * V3_CLIENT_TEXT.noArbiterVeto, which this package cannot import —
60
+ * v3-acceptance-veto-disclosure.test.ts holds the two equal.
61
+ */
62
+ export declare const NO_ARBITER_VETO = "No arbiter is named. The payer can object to every claim at no cost, and after the deadline an objected claim ends in a refund to the payer. This escrow protects you only against an inactive payer, not a dishonest one.";
56
63
  export declare function accept(ctx: SignerContext, input: AcceptInput): Promise<{
57
64
  accepted: boolean;
58
65
  escrowId: string;
@@ -8,7 +8,7 @@
8
8
  * is rebuilt here from checked values, under the PINNED domain (this package's
9
9
  * chain ID and contract), with the contract's own types.
10
10
  */
11
- import { ZeroAddress, getAddress, isAddress, parseEther } from "ethers";
11
+ import { ZeroAddress, formatEther, getAddress, isAddress, parseEther } from "ethers";
12
12
  import { refuse } from "./errors.js";
13
13
  import { ACCEPTANCE_PRIMARY_TYPE, ACCEPTANCE_TYPES, V3_LIMITS, acceptanceDomain, escrowIdFor } from "./calldata.js";
14
14
  import { latestBlock } from "./chain.js";
@@ -16,6 +16,7 @@ import { ACCEPT_BY_MARGIN_SECONDS } from "./checks.js";
16
16
  import { Book } from "./book.js";
17
17
  import { loadPolicy } from "./policy.js";
18
18
  import { verifyEndpoint } from "./network.js";
19
+ import { confirmOnTerminal } from "./actions.js";
19
20
  const lc = (s) => String(s ?? "").toLowerCase();
20
21
  /** The floor of the payee's work window, whatever the review period and term (the app's MIN_WORK_FLOOR_SEC). */
21
22
  export const MIN_WORK_FLOOR_SECONDS = 72 * 3600;
@@ -117,6 +118,13 @@ export function checkAcceptance(o, ctx) {
117
118
  refuse("ACCEPTANCE_CHECK_FAILED", `refused before signing: ${r.join("; ")}`);
118
119
  return msg;
119
120
  }
121
+ /**
122
+ * The free veto of an escrow without an arbiter (cascrow S-05, accepted on the
123
+ * condition that the payee knows before signing). The same words as cascrow's
124
+ * V3_CLIENT_TEXT.noArbiterVeto, which this package cannot import —
125
+ * v3-acceptance-veto-disclosure.test.ts holds the two equal.
126
+ */
127
+ export const NO_ARBITER_VETO = "No arbiter is named. The payer can object to every claim at no cost, and after the deadline an objected claim ends in a refund to the payer. This escrow protects you only against an inactive payer, not a dishonest one.";
120
128
  export async function accept(ctx, input) {
121
129
  if (!input.milestoneId)
122
130
  refuse("INPUT", "milestoneId is required");
@@ -141,6 +149,17 @@ export async function accept(ctx, input) {
141
149
  arbiters,
142
150
  minAmountWei: input.minAmountXrp ? parseEther(input.minAmountXrp) : null,
143
151
  });
152
+ // A person at a terminal sees the terms that will bind them and says yes (S-02).
153
+ await confirmOnTerminal(ctx, "Sign this acceptance?", [
154
+ `You are about to ACCEPT an escrow offer on ${ctx.net.name} (chain ${ctx.net.chainId}, contract ${ctx.net.address}):`,
155
+ ` payer: ${getAddress(offer.payer)}`,
156
+ ` amount: ${formatEther(msg.amount)} XRP`,
157
+ ` deadline: ${new Date(Number(msg.deadline) * 1000).toISOString()} — your work must be claimed before then`,
158
+ ` review period: ${(msg.reviewPeriod / 3600).toFixed(1)} h — the payer's time to object to your claim`,
159
+ ` arbiter: ${msg.arbiter === ZeroAddress ? "none" : `${msg.arbiter} (arbitration period ${(msg.arbitrationPeriod / 86400).toFixed(1)} days)`}`,
160
+ ` binding until ${new Date(Number(msg.acceptBy) * 1000).toISOString()} — the payer can fund these exact terms until then, and nothing in cascrow revokes it`,
161
+ ...(msg.arbiter === ZeroAddress ? [` WARNING: ${NO_ARBITER_VETO}`] : []),
162
+ ]);
144
163
  const signature = await key.signTypedData(acceptanceDomain(ctx.net.chainId, ctx.net.address), ACCEPTANCE_TYPES, { ...msg });
145
164
  const answer = await ctx.remote.submitAcceptance(offer.offerId, signature);
146
165
  new Book(ctx.home, key.macKey).add({
@@ -164,6 +183,7 @@ export async function accept(ctx, input) {
164
183
  reviewPeriod: msg.reviewPeriod,
165
184
  arbiter: msg.arbiter,
166
185
  server: { escrowId: answer.escrowId ?? null, acceptBy: answer.acceptBy ?? null },
167
- note: "Your signature binds you until acceptBy: the payer can fund these exact terms until then, and nothing in cascrow revokes it.",
186
+ note: "Your signature binds you until acceptBy: the payer can fund these exact terms until then, and nothing in cascrow revokes it." +
187
+ (msg.arbiter === ZeroAddress ? ` ${NO_ARBITER_VETO}` : ""),
168
188
  };
169
189
  }
package/dist/actions.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { PER_ESCROW_ACTIONS } from "./calldata.js";
2
2
  import type { SignerAction } from "./calldata.js";
3
+ import type { FundTerms } from "./checks.js";
4
+ import type { SendResult } from "./chain.js";
5
+ import type { UnlockedKey } from "./keystore.js";
3
6
  import { RemoteError } from "./remote.js";
4
7
  import type { V3EscrowAction } from "./generated/v3-actions.js";
5
8
  import type { SignerContext } from "./context.js";
@@ -13,6 +16,14 @@ export type ActionResult = {
13
16
  notes: string[];
14
17
  [k: string]: unknown;
15
18
  };
19
+ /**
20
+ * On a terminal: show what is about to be signed and ask y/N; a "no" signs nothing
21
+ * (DECLINED). Off a terminal (the MCP server) this does nothing — the policy is what
22
+ * bounds an agent. A terminal Io without a way to ask is refused, not waved through.
23
+ */
24
+ export declare function confirmOnTerminal(ctx: SignerContext, question: string, lines: string[]): Promise<void>;
25
+ /** What a payer's `fund` commits to, in the words a person checks (S-02). */
26
+ export declare function fundSummary(ctx: Pick<SignerContext, "net">, f: FundTerms, payeeKnown: boolean): string[];
16
27
  export type FundInput = {
17
28
  milestoneId: string;
18
29
  maxAmountXrp?: string;
@@ -38,7 +49,10 @@ export declare function localProofHash(milestoneId: string, ev: EvidenceInput):
38
49
  export type EscrowActionInput = {
39
50
  escrowId?: string;
40
51
  milestoneId?: string;
41
- /** object: the objection reason; its keccak256 goes on chain. */
52
+ /**
53
+ * object: the objection reason. On chain goes keccak256(salt ‖ utf8(reason)) with a
54
+ * salt made here (reason.ts); the reason and the salt never leave this machine.
55
+ */
42
56
  reason?: string;
43
57
  /** extendDeadline: unix seconds. */
44
58
  newDeadline?: string | number;
@@ -57,4 +71,16 @@ export declare function isPrepareRefusal(e: unknown): e is RemoteError;
57
71
  export declare function escrowAction(ctx: SignerContext, action: V3EscrowAction, input: EscrowActionInput): Promise<ActionResult>;
58
72
  /** withdraw(own account, native XRP): pays this key's credit to this key. No escrow, no cascrow. */
59
73
  export declare function withdraw(ctx: SignerContext): Promise<ActionResult>;
74
+ /**
75
+ * Replace one of this signer's pending transactions with the same nonce and a 25 % higher
76
+ * fee — the way to get a pending objection mined. Call inside `withLock`.
77
+ *
78
+ * Re-signing is signing, so it is checked like a first signature (S-01): the entry must
79
+ * be AUTHENTIC (in the verified part of the journal — never a 0.1.0 line, never a line
80
+ * after a break), it must move no value (a funding is never replaced: its limit was
81
+ * checked for the transaction as signed), its calldata must decode to the action it was
82
+ * reserved for and name the same escrow, and the action is checked against the chain and
83
+ * the policy again exactly as `escrowAction` checks it. Only then is it re-signed.
84
+ */
85
+ export declare function speedUp(ctx: SignerContext, key: UnlockedKey, txHash: string): Promise<SendResult>;
60
86
  export { PER_ESCROW_ACTIONS };