cascrow-signer 0.1.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 +28 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +35 -0
- package/README.md +130 -0
- package/SECURITY.md +71 -0
- package/dist/acceptance.d.ts +70 -0
- package/dist/acceptance.js +169 -0
- package/dist/actions.d.ts +60 -0
- package/dist/actions.js +319 -0
- package/dist/api.d.ts +83 -0
- package/dist/api.js +105 -0
- package/dist/book.d.ts +26 -0
- package/dist/book.js +62 -0
- package/dist/calldata.d.ts +62 -0
- package/dist/calldata.js +121 -0
- package/dist/chain.d.ts +59 -0
- package/dist/chain.js +233 -0
- package/dist/checks.d.ts +101 -0
- package/dist/checks.js +236 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +163 -0
- package/dist/context.d.ts +47 -0
- package/dist/context.js +50 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +17 -0
- package/dist/files.d.ts +13 -0
- package/dist/files.js +78 -0
- package/dist/generated/milestone-escrow-v3.deployments.json +15 -0
- package/dist/generated/milestone-escrow-v3.json +1179 -0
- package/dist/generated/v3-actions.d.ts +49 -0
- package/dist/generated/v3-actions.js +102 -0
- package/dist/generated/v3-proof-manifest.d.ts +19 -0
- package/dist/generated/v3-proof-manifest.js +79 -0
- package/dist/journal.d.ts +41 -0
- package/dist/journal.js +186 -0
- package/dist/keystore.d.ts +30 -0
- package/dist/keystore.js +119 -0
- package/dist/link.d.ts +17 -0
- package/dist/link.js +65 -0
- package/dist/mcp.d.ts +10 -0
- package/dist/mcp.js +81 -0
- package/dist/network.d.ts +53 -0
- package/dist/network.js +122 -0
- package/dist/policy.d.ts +31 -0
- package/dist/policy.js +92 -0
- package/dist/remote.d.ts +31 -0
- package/dist/remote.js +104 -0
- package/dist/tools.d.ts +20 -0
- package/dist/tools.js +112 -0
- package/dist/watch.d.ts +35 -0
- package/dist/watch.js +129 -0
- package/package.json +64 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — unreleased (owner publishes)
|
|
4
|
+
|
|
5
|
+
- First version. A local signer for cascrow's MilestoneEscrowV3 escrows: `init` (key and
|
|
6
|
+
encrypted keystore on this machine), `policy` (limits in XRP, counterparty and arbiter
|
|
7
|
+
allowlists, minimum review period — terminal only, MAC-protected), `link`, `accept`,
|
|
8
|
+
`fund`, `submit-claim`, `claim`, `release`, `object`, `refund`, `extend-deadline`,
|
|
9
|
+
`return-funds`, `withdraw`, `claims`, `watch`, `speed-up`, and an MCP server (`mcp`)
|
|
10
|
+
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).
|
|
14
|
+
- One runtime dependency, `ethers` 6.17.0, pinned exactly, with `npm-shrinkwrap.json`.
|
|
15
|
+
- `submit-claim` takes `--proof-hash` (MCP: `proofHash`) — the `manifestHash` the evidence
|
|
16
|
+
door returned — and signs only after cascrow confirms it is a sealed submission of the
|
|
17
|
+
milestone; a hash computed from a local file must be confirmed the same way
|
|
18
|
+
(`CLAIM_PROOF_UNKNOWN` otherwise, no override).
|
|
19
|
+
- A cascrow `prepare` that answers with a refusal (4xx and its code) is surfaced as that
|
|
20
|
+
refusal, with its remedy; only an unreachable cascrow, a 5xx, a non-JSON answer, 401, 408
|
|
21
|
+
or 429 count as "not available" and let a post-funding action proceed from the chain.
|
|
22
|
+
- `object` is gated on the chain alone (the on-chain payer is this key); only `release` and
|
|
23
|
+
`extend-deadline` need the local book, and a damaged book is a loud note, not an abort,
|
|
24
|
+
for every other action.
|
|
25
|
+
- The CLI removes `CASCROW_KEYSTORE_PASSPHRASE` and `_FILE` from its environment at start
|
|
26
|
+
for every command; a `watch --exec` command gets an allowlisted environment.
|
|
27
|
+
- `accept` refuses an offer that leaves the payee less than max(review period, 72 h, 25 %
|
|
28
|
+
of the term) between `acceptBy` and the deadline.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cascrow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Migrating to cascrow-signer
|
|
2
|
+
|
|
3
|
+
## From `cascrow_escrow_fund` / `escrowFund()` / `cascrow escrow-fund --private-key`
|
|
4
|
+
|
|
5
|
+
Those paths sent your private key to cascrow, which signed the deposit on its servers.
|
|
6
|
+
They were removed in cascrow-mcp 2.0.0, cascrow-sdk 0.5.0 and cascrow-cli 2.0.0.
|
|
7
|
+
**If you ever used one, treat that key as exposed**: move its funds to a new wallet, and
|
|
8
|
+
do not link the old address again (cascrow refuses it anyway).
|
|
9
|
+
|
|
10
|
+
| Before | Now |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `cascrow_escrow_fund { agentPrivateKey, … }` | `cascrow_signer_fund { milestoneId }` in this package's MCP server |
|
|
13
|
+
| `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>` |
|
|
15
|
+
| the wallet cascrow generated at registration | `cascrow-signer init` + `cascrow-signer link` |
|
|
16
|
+
|
|
17
|
+
## v2 (RLUSD) escrows
|
|
18
|
+
|
|
19
|
+
Unchanged: `GET /api/agent/escrow-prepare` returns the two unsigned transactions (approve,
|
|
20
|
+
fund); sign and broadcast them with your own wallet, then `POST /api/escrow/sync`. This
|
|
21
|
+
package signs MilestoneEscrowV3 only.
|
|
22
|
+
|
|
23
|
+
## MilestoneEscrowV3, step by step
|
|
24
|
+
|
|
25
|
+
1. Payer: `cascrow_v3_offer` (remote, cascrow-mcp / SDK `v3.offer` / `cascrow v3 offer`).
|
|
26
|
+
2. Payee: `cascrow-signer accept --milestone <id>`.
|
|
27
|
+
3. Payer: `cascrow-signer fund --milestone <id>`.
|
|
28
|
+
4. Payee: work, submit evidence to cascrow for the AI recommendation, then
|
|
29
|
+
`cascrow-signer submit-claim --milestone <id> --file …`.
|
|
30
|
+
5. Payer: within the review period, `release` — or `object` with a reason. `watch` makes
|
|
31
|
+
sure you see the claim; cascrow's notifications are a convenience, not the guarantee.
|
|
32
|
+
|
|
33
|
+
A cascrow verification of `YES` on a V3 milestone is a **recommendation** and moves no
|
|
34
|
+
money: only the payer's `release`, an unobjected claim running out its review period
|
|
35
|
+
(`claim`), or the arbiter pays the payee.
|
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# cascrow-signer
|
|
2
|
+
|
|
3
|
+
Your key for cascrow's **MilestoneEscrowV3** escrows — generated, encrypted and used on
|
|
4
|
+
your machine only. cascrow verifies work, keeps the audit trail and prepares transactions;
|
|
5
|
+
it never holds, receives or uses your key, and it never sends, relays or pays for a
|
|
6
|
+
transaction on your behalf. This package is the part that signs, and it checks everything
|
|
7
|
+
it signs against what it knows independently of cascrow.
|
|
8
|
+
|
|
9
|
+
- One runtime dependency (`ethers`, pinned exactly) and a shrinkwrap, so the code that
|
|
10
|
+
touches your key can be read end to end.
|
|
11
|
+
- The chain, the public RPC and the escrow contract's address and code hash are **pinned in
|
|
12
|
+
the package**. A network without a pinned deployment is refused.
|
|
13
|
+
- Transactions go to the network's **public RPC**, never to cascrow.
|
|
14
|
+
|
|
15
|
+
Install a pinned version — never `@latest` for a tool that holds money:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npx cascrow-signer@0.1.0 help
|
|
19
|
+
```
|
|
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*).
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
export CASCROW_SIGNER_NETWORK=testnet # or mainnet — never guessed
|
|
29
|
+
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
|
|
31
|
+
export CASCROW_API_KEY=csk_… # your cascrow account
|
|
32
|
+
cascrow-signer link # proves this address is yours to cascrow
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`~/.cascrow/signer` is created `0700`, every file in it `0600`; a file other users can
|
|
36
|
+
read is refused, not repaired. Set `CASCROW_SIGNER_HOME` to use another directory.
|
|
37
|
+
|
|
38
|
+
**Passphrase**: a terminal prompt, `CASCROW_KEYSTORE_PASSPHRASE_FILE` (a `0600` file), or
|
|
39
|
+
`CASCROW_KEYSTORE_PASSPHRASE`. Either variable is removed from the process environment once
|
|
40
|
+
read. There is no OS-keychain integration on purpose (native dependency, supply chain).
|
|
41
|
+
|
|
42
|
+
## As payer
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
cascrow-signer fund --milestone <id> # one transaction, value = amount, no approve
|
|
46
|
+
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: …"
|
|
49
|
+
cascrow-signer release --escrow 0x…
|
|
50
|
+
cascrow-signer extend-deadline --escrow 0x… --new-deadline 1767225600
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Before `fund` signs, it checks — against values it knows itself, not the server's word:
|
|
54
|
+
the escrow ID is `escrowIdFor(your address, ref, salt)`; the token is native XRP; `value`
|
|
55
|
+
equals the amount; the amount is within your per-escrow limit (a lower one for a payee not
|
|
56
|
+
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.
|
|
60
|
+
|
|
61
|
+
After funding, `object`, `release`, `refund`, `extend-deadline`, `claim`, `withdraw` and
|
|
62
|
+
`return-funds` are built from the escrow ID and the chain alone — **cascrow does not need
|
|
63
|
+
to be reachable** for you to object. `object` is signed whenever the chain names you as
|
|
64
|
+
the escrow's payer — it does not need the local book, so a lost or damaged book never
|
|
65
|
+
costs you an objection window. `release` and `extend-deadline` are signed only for
|
|
66
|
+
escrows in your local book whose on-chain payer is you. When cascrow is reachable and
|
|
67
|
+
**refuses** to prepare an action (an answer with a code, e.g. `WRONG_PHASE` or
|
|
68
|
+
`COMPLIANCE_BLOCK`), the signer stops and shows that refusal instead of sending anyway;
|
|
69
|
+
only an unreachable cascrow, a 5xx, a rate limit or a bad API key count as "not available".
|
|
70
|
+
|
|
71
|
+
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.
|
|
73
|
+
|
|
74
|
+
## As payee
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
cascrow-signer accept --milestone <id> # checks and signs the payer's offer
|
|
78
|
+
cascrow-signer submit-claim --milestone <id> --proof-hash 0x… # the manifestHash the upload returned
|
|
79
|
+
cascrow-signer return-funds --escrow 0x…
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`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
|
|
85
|
+
cascrow revokes it.
|
|
86
|
+
|
|
87
|
+
`submit-claim` claims with ONE sealed submission. Every evidence upload (one request —
|
|
88
|
+
today one file, one text or one GitHub item) is its own submission, and its answer carries
|
|
89
|
+
the submission's `manifestHash`: pass that as `--proof-hash`. Instead you can pass the one
|
|
90
|
+
file of that submission (`--file report.pdf`) and the hash is computed here with the same
|
|
91
|
+
manifest encoding cascrow uses. Either way cascrow must confirm the hash is a sealed
|
|
92
|
+
submission of the milestone (`CLAIM_PROOF_UNKNOWN` otherwise) before anything is signed — a
|
|
93
|
+
claim naming evidence cascrow never assessed has no recommendation and invites an
|
|
94
|
+
objection. Claim with a submission whose verdict is YES (`claimReady`); with NO or PENDING
|
|
95
|
+
the payer is likely to object.
|
|
96
|
+
|
|
97
|
+
## As an MCP server
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{ "mcpServers": { "cascrow-signer": {
|
|
101
|
+
"command": "npx", "args": ["cascrow-signer@0.1.0", "mcp"],
|
|
102
|
+
"env": { "CASCROW_SIGNER_NETWORK": "testnet", "CASCROW_API_KEY": "csk_…",
|
|
103
|
+
"CASCROW_KEYSTORE_PASSPHRASE_FILE": "/home/me/.cascrow/pass" } } } }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Tools: `cascrow_signer_status`, `_link`, `_accept`, `_fund`, `_submit_claim`, `_object`,
|
|
107
|
+
`_release`, `_extend_deadline`, `_claim`, `_refund`, `_return_funds`, `_withdraw`,
|
|
108
|
+
`_onchain_claims`, `_speed_up`. They are all local; cascrow's hosted MCP server and the
|
|
109
|
+
`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.
|
|
111
|
+
|
|
112
|
+
Use it next to `cascrow-mcp` (or the hosted server), which carries the remote
|
|
113
|
+
`cascrow_v3_*` tools: offers, acceptance data, prepared transactions, attestations.
|
|
114
|
+
|
|
115
|
+
## Testnet override
|
|
116
|
+
|
|
117
|
+
For a testnet deployment the package does not pin yet — only one you deployed or verified
|
|
118
|
+
yourself:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
CASCROW_SIGNER_TESTNET_CONTRACT=0x… CASCROW_SIGNER_TESTNET_DEPLOY_BLOCK=123 \
|
|
122
|
+
CASCROW_SIGNER_TESTNET_CODE_HASH=0x… [CASCROW_SIGNER_TESTNET_RPC_URL=…] cascrow-signer status
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Every use prints a warning. There is no override for mainnet.
|
|
126
|
+
|
|
127
|
+
## Windows
|
|
128
|
+
|
|
129
|
+
POSIX permission checks are skipped on Windows; protect `%USERPROFILE%\.cascrow\signer`
|
|
130
|
+
yourself. See [SECURITY.md](SECURITY.md) for what this package does and does not protect.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Security — what cascrow-signer protects, and what it does not
|
|
2
|
+
|
|
3
|
+
## The boundary
|
|
4
|
+
|
|
5
|
+
cascrow (the service) prepares, reads and recommends. It never creates, holds, receives
|
|
6
|
+
or uses a party's key, and never sends, relays or pays for a transaction to the escrow
|
|
7
|
+
contract (rule I9). This package is where the key lives. The line between the two:
|
|
8
|
+
|
|
9
|
+
| Leaves this machine for cascrow | Never leaves this machine |
|
|
10
|
+
|---|---|
|
|
11
|
+
| the wallet-link signature over cascrow's fixed challenge sentence | the private key |
|
|
12
|
+
| the payee's acceptance signature (EIP-712, pinned domain) | the passphrase |
|
|
13
|
+
| a transaction **hash**, as a hint to re-read the chain | the keystore file |
|
|
14
|
+
| your API key, as the `Authorization` header | any signed transaction |
|
|
15
|
+
|
|
16
|
+
Signed transactions go to the network's public RPC only (pinned in the package). The
|
|
17
|
+
complete list of calls to cascrow is in `src/remote.ts`. A test in the cascrow repository
|
|
18
|
+
(`signer-canary.test.ts`) runs every action with a random key and passphrase against a fake
|
|
19
|
+
cascrow and a fake RPC and fails if any URL, header, body, stdout/stderr line or tool
|
|
20
|
+
result contains the key, the passphrase, the keystore ciphertext or a raw signed
|
|
21
|
+
transaction.
|
|
22
|
+
|
|
23
|
+
## What the checks protect against
|
|
24
|
+
|
|
25
|
+
- **A wrong or compromised cascrow API.** Chain ID, contract address and runtime code hash
|
|
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
|
|
28
|
+
domain; `data` is rebuilt from the ABI. A server can refuse to help you, but it cannot
|
|
29
|
+
make this signer fund different terms, pay a different contract or sign a message that is
|
|
30
|
+
not the wallet challenge.
|
|
31
|
+
- **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
|
|
34
|
+
carries a MAC under a key derived from your private key.
|
|
35
|
+
- **Losing cascrow.** Every action after funding but `submitClaim` works from the chain
|
|
36
|
+
alone; the watcher reads your escrows from the chain. `object` needs neither cascrow nor
|
|
37
|
+
the local book — only that the chain names this key as the payer. `submitClaim` needs
|
|
38
|
+
cascrow's confirmation that the proof hash is a sealed submission of the milestone.
|
|
39
|
+
- **Cheating the payee on time.** `accept` refuses an offer whose `acceptBy` leaves less
|
|
40
|
+
than max(review period, 72 h, 25 % of the remaining term) before the deadline: the payer
|
|
41
|
+
can fund at any moment up to `acceptBy`, so what follows `acceptBy` is the payee's
|
|
42
|
+
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.
|
|
45
|
+
|
|
46
|
+
## What it does NOT protect against
|
|
47
|
+
|
|
48
|
+
- **A compromised cascrow server, on the payee's side, within your own limits.** A payee's
|
|
49
|
+
acceptance signature can be funded by the payer until `acceptBy`; the server only relays
|
|
50
|
+
it. A compromised server can therefore get a fund to a payee who signed an acceptance —
|
|
51
|
+
never different terms, never above your local limits. In MCP mode (no terminal) a payee
|
|
52
|
+
not in your counterparty book is funded only up to `unknownPayeePerEscrowMaxWei`, so that
|
|
53
|
+
is the most a compromised server plus a driven agent can place with an unknown payee per
|
|
54
|
+
escrow (and your 24-hour limit across escrows). Set it low, or add payees on a terminal.
|
|
55
|
+
And until `acceptBy` the payer holds a free option to fund or let the offer lapse; the
|
|
56
|
+
payee's minimum work window (above) bounds what that option can cost the payee.
|
|
57
|
+
- **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
|
|
59
|
+
its environment or can read the passphrase file, the policy stops honest mistakes, not
|
|
60
|
+
that agent. Run the signer as a separate user or on a separate machine if that matters.
|
|
61
|
+
- **Malware on the machine**, a compromised Node.js, or a compromised `ethers` (pinned
|
|
62
|
+
exactly and shrinkwrapped, so an upgrade is a visible change — not a guarantee).
|
|
63
|
+
- **A compromised release of this package.** Install a pinned version and check it.
|
|
64
|
+
- **The public RPC lying about chain state** (it cannot forge your signature, but it could
|
|
65
|
+
hide a claim from `watch`). Point `watch` at a second RPC you trust if the review window
|
|
66
|
+
is short.
|
|
67
|
+
- **Windows file permissions** — not checked there.
|
|
68
|
+
|
|
69
|
+
## Reporting
|
|
70
|
+
|
|
71
|
+
security@cascrow.com. Please do not open a public issue for a vulnerability.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { SignerNetwork } from "./network.js";
|
|
2
|
+
import type { SignerContext } from "./context.js";
|
|
3
|
+
export type AcceptanceOffer = {
|
|
4
|
+
offerId: string;
|
|
5
|
+
escrowId: string;
|
|
6
|
+
payer: string;
|
|
7
|
+
ref: string;
|
|
8
|
+
salt: string;
|
|
9
|
+
amountWei: string;
|
|
10
|
+
acceptBy: string | number;
|
|
11
|
+
typedData: {
|
|
12
|
+
domain: Record<string, unknown>;
|
|
13
|
+
types: Record<string, {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
}[]>;
|
|
17
|
+
primaryType: string;
|
|
18
|
+
message: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export type AcceptanceMessage = {
|
|
22
|
+
escrowId: string;
|
|
23
|
+
payee: string;
|
|
24
|
+
token: string;
|
|
25
|
+
amount: bigint;
|
|
26
|
+
deadline: bigint;
|
|
27
|
+
reviewPeriod: number;
|
|
28
|
+
arbiter: string;
|
|
29
|
+
arbitrationPeriod: number;
|
|
30
|
+
acceptBy: bigint;
|
|
31
|
+
};
|
|
32
|
+
/** The floor of the payee's work window, whatever the review period and term (the app's MIN_WORK_FLOOR_SEC). */
|
|
33
|
+
export declare const MIN_WORK_FLOOR_SECONDS: number;
|
|
34
|
+
/**
|
|
35
|
+
* The least time the payee must have between the last moment the payer can fund
|
|
36
|
+
* (`acceptBy`) and the deadline: max(review period, 72 h, 25 % of the remaining
|
|
37
|
+
* term). The same formula cascrow applies when it issues the offer
|
|
38
|
+
* (`minWorkWindowSec` in the app's v3-terms.ts). Checked HERE as well, because the
|
|
39
|
+
* payer holds a free fund-or-lapse option until `acceptBy`: a compromised server
|
|
40
|
+
* could otherwise hand the payee an offer whose acceptBy sits right before the
|
|
41
|
+
* deadline, and a funding at the last moment would leave no time to deliver.
|
|
42
|
+
*/
|
|
43
|
+
export declare function minWorkWindowSeconds(reviewPeriodSec: number, deadlineSec: bigint | number, nowSec: bigint | number): number;
|
|
44
|
+
/** Every check on an offer before the payee signs it. Pure. Returns the message to sign, rebuilt from checked values. */
|
|
45
|
+
export declare function checkAcceptance(o: AcceptanceOffer, ctx: {
|
|
46
|
+
me: string;
|
|
47
|
+
net: Pick<SignerNetwork, "chainId" | "address">;
|
|
48
|
+
chainNowSec: number;
|
|
49
|
+
arbiters: readonly string[];
|
|
50
|
+
minAmountWei?: bigint | null;
|
|
51
|
+
}): AcceptanceMessage;
|
|
52
|
+
export type AcceptInput = {
|
|
53
|
+
milestoneId: string;
|
|
54
|
+
minAmountXrp?: string;
|
|
55
|
+
};
|
|
56
|
+
export declare function accept(ctx: SignerContext, input: AcceptInput): Promise<{
|
|
57
|
+
accepted: boolean;
|
|
58
|
+
escrowId: string;
|
|
59
|
+
payer: string;
|
|
60
|
+
amountWei: string;
|
|
61
|
+
acceptBy: string;
|
|
62
|
+
deadline: string;
|
|
63
|
+
reviewPeriod: number;
|
|
64
|
+
arbiter: string;
|
|
65
|
+
server: {
|
|
66
|
+
escrowId: {} | null;
|
|
67
|
+
acceptBy: {} | null;
|
|
68
|
+
};
|
|
69
|
+
note: string;
|
|
70
|
+
}>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The payee's acceptance, signed on this machine after checking it against what
|
|
3
|
+
* the payee knows independently of cascrow.
|
|
4
|
+
*
|
|
5
|
+
* The signature is irrevocable until `acceptBy`: whoever holds it can fund the
|
|
6
|
+
* escrow with exactly these terms, and deleting anything in cascrow does not take
|
|
7
|
+
* it back. So nothing from the server's typed data is signed as-is — the message
|
|
8
|
+
* is rebuilt here from checked values, under the PINNED domain (this package's
|
|
9
|
+
* chain ID and contract), with the contract's own types.
|
|
10
|
+
*/
|
|
11
|
+
import { ZeroAddress, getAddress, isAddress, parseEther } from "ethers";
|
|
12
|
+
import { refuse } from "./errors.js";
|
|
13
|
+
import { ACCEPTANCE_PRIMARY_TYPE, ACCEPTANCE_TYPES, V3_LIMITS, acceptanceDomain, escrowIdFor } from "./calldata.js";
|
|
14
|
+
import { latestBlock } from "./chain.js";
|
|
15
|
+
import { ACCEPT_BY_MARGIN_SECONDS } from "./checks.js";
|
|
16
|
+
import { Book } from "./book.js";
|
|
17
|
+
import { loadPolicy } from "./policy.js";
|
|
18
|
+
import { verifyEndpoint } from "./network.js";
|
|
19
|
+
const lc = (s) => String(s ?? "").toLowerCase();
|
|
20
|
+
/** The floor of the payee's work window, whatever the review period and term (the app's MIN_WORK_FLOOR_SEC). */
|
|
21
|
+
export const MIN_WORK_FLOOR_SECONDS = 72 * 3600;
|
|
22
|
+
/**
|
|
23
|
+
* The least time the payee must have between the last moment the payer can fund
|
|
24
|
+
* (`acceptBy`) and the deadline: max(review period, 72 h, 25 % of the remaining
|
|
25
|
+
* term). The same formula cascrow applies when it issues the offer
|
|
26
|
+
* (`minWorkWindowSec` in the app's v3-terms.ts). Checked HERE as well, because the
|
|
27
|
+
* payer holds a free fund-or-lapse option until `acceptBy`: a compromised server
|
|
28
|
+
* could otherwise hand the payee an offer whose acceptBy sits right before the
|
|
29
|
+
* deadline, and a funding at the last moment would leave no time to deliver.
|
|
30
|
+
*/
|
|
31
|
+
export function minWorkWindowSeconds(reviewPeriodSec, deadlineSec, nowSec) {
|
|
32
|
+
const term = Number(BigInt(deadlineSec) - BigInt(nowSec));
|
|
33
|
+
return Math.max(reviewPeriodSec, MIN_WORK_FLOOR_SECONDS, Math.floor(Math.max(0, term) * 0.25));
|
|
34
|
+
}
|
|
35
|
+
/** Every check on an offer before the payee signs it. Pure. Returns the message to sign, rebuilt from checked values. */
|
|
36
|
+
export function checkAcceptance(o, ctx) {
|
|
37
|
+
const r = [];
|
|
38
|
+
if (!o || typeof o !== "object" || !o.typedData || typeof o.typedData !== "object")
|
|
39
|
+
refuse("ACCEPTANCE_CHECK_FAILED", "refused before signing: no typed data");
|
|
40
|
+
const d = o.typedData.domain ?? {};
|
|
41
|
+
const pinned = acceptanceDomain(ctx.net.chainId, ctx.net.address);
|
|
42
|
+
if (d.name !== pinned.name || d.version !== pinned.version)
|
|
43
|
+
r.push("the domain name/version is not the contract's");
|
|
44
|
+
if (String(d.chainId) !== String(pinned.chainId))
|
|
45
|
+
r.push("the domain chainId is not the pinned chain");
|
|
46
|
+
if (lc(d.verifyingContract) !== lc(pinned.verifyingContract))
|
|
47
|
+
r.push("the domain verifyingContract is not the pinned contract");
|
|
48
|
+
const types = { ...(o.typedData.types ?? {}) };
|
|
49
|
+
delete types.EIP712Domain;
|
|
50
|
+
if (JSON.stringify(types) !== JSON.stringify(ACCEPTANCE_TYPES))
|
|
51
|
+
r.push("the types are not the contract's PayeeAcceptance");
|
|
52
|
+
if (o.typedData.primaryType !== ACCEPTANCE_PRIMARY_TYPE)
|
|
53
|
+
r.push("primaryType is not PayeeAcceptance");
|
|
54
|
+
const m = o.typedData.message ?? {};
|
|
55
|
+
let msg = null;
|
|
56
|
+
try {
|
|
57
|
+
msg = {
|
|
58
|
+
escrowId: lc(m.escrowId),
|
|
59
|
+
payee: getAddress(String(m.payee)),
|
|
60
|
+
token: getAddress(String(m.token)),
|
|
61
|
+
amount: BigInt(String(m.amount)),
|
|
62
|
+
deadline: BigInt(String(m.deadline)),
|
|
63
|
+
reviewPeriod: Number(m.reviewPeriod),
|
|
64
|
+
arbiter: getAddress(String(m.arbiter)),
|
|
65
|
+
arbitrationPeriod: Number(m.arbitrationPeriod),
|
|
66
|
+
acceptBy: BigInt(String(m.acceptBy)),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
refuse("ACCEPTANCE_CHECK_FAILED", "refused before signing: the message does not parse");
|
|
71
|
+
}
|
|
72
|
+
if (msg.payee !== getAddress(ctx.me))
|
|
73
|
+
r.push("the payee is not this key");
|
|
74
|
+
if (!isAddress(o.payer) || getAddress(o.payer) === getAddress(ctx.me))
|
|
75
|
+
r.push("the payer is missing or is this key");
|
|
76
|
+
else if (lc(escrowIdFor(o.payer, o.ref, o.salt)) !== msg.escrowId)
|
|
77
|
+
r.push("escrowId is not escrowIdFor(payer, ref, salt)");
|
|
78
|
+
if (lc(o.escrowId) !== msg.escrowId)
|
|
79
|
+
r.push("the offer's escrowId and the message's differ");
|
|
80
|
+
if (msg.token !== ZeroAddress)
|
|
81
|
+
r.push("the token is not native XRP");
|
|
82
|
+
if (msg.amount <= BigInt(0) || String(o.amountWei) !== msg.amount.toString())
|
|
83
|
+
r.push("the amount is zero or differs from the offer's amountWei");
|
|
84
|
+
if (ctx.minAmountWei && msg.amount < ctx.minAmountWei)
|
|
85
|
+
r.push("the amount is below the minimum you named");
|
|
86
|
+
if (String(o.acceptBy) !== msg.acceptBy.toString() && String(Math.floor(Date.parse(String(o.acceptBy)) / 1000)) !== msg.acceptBy.toString()) {
|
|
87
|
+
r.push("the offer's acceptBy and the message's differ");
|
|
88
|
+
}
|
|
89
|
+
const now = BigInt(ctx.chainNowSec);
|
|
90
|
+
if (msg.acceptBy < now + BigInt(ACCEPT_BY_MARGIN_SECONDS))
|
|
91
|
+
r.push("acceptBy has passed on chain (or is within two minutes)");
|
|
92
|
+
if (msg.deadline <= msg.acceptBy)
|
|
93
|
+
r.push("the deadline is not after acceptBy");
|
|
94
|
+
else {
|
|
95
|
+
const minWork = minWorkWindowSeconds(msg.reviewPeriod, msg.deadline, now);
|
|
96
|
+
if (msg.acceptBy > msg.deadline - BigInt(minWork)) {
|
|
97
|
+
r.push(`a funding at acceptBy would leave less than ${Math.ceil(minWork / 3600)} h before the deadline to do the work (max of the review period, 72 h and 25 % of the term)`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (msg.deadline > now + BigInt(V3_LIMITS.MAX_DEADLINE_SECONDS))
|
|
101
|
+
r.push("the deadline is beyond the contract's maximum duration");
|
|
102
|
+
if (msg.reviewPeriod < V3_LIMITS.MIN_REVIEW_SECONDS || msg.reviewPeriod > V3_LIMITS.MAX_REVIEW_SECONDS)
|
|
103
|
+
r.push("the review period is outside the contract's bounds");
|
|
104
|
+
if (msg.arbiter === ZeroAddress) {
|
|
105
|
+
if (msg.arbitrationPeriod !== 0)
|
|
106
|
+
r.push("arbitrationPeriod without an arbiter");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (!ctx.arbiters.includes(msg.arbiter.toLowerCase()))
|
|
110
|
+
r.push("the arbiter is not one your local policy accepts");
|
|
111
|
+
if (msg.arbiter === msg.payee || msg.arbiter === getAddress(o.payer))
|
|
112
|
+
r.push("the arbiter is a party");
|
|
113
|
+
}
|
|
114
|
+
if (typeof o.offerId !== "string" || !o.offerId)
|
|
115
|
+
r.push("no offerId");
|
|
116
|
+
if (r.length)
|
|
117
|
+
refuse("ACCEPTANCE_CHECK_FAILED", `refused before signing: ${r.join("; ")}`);
|
|
118
|
+
return msg;
|
|
119
|
+
}
|
|
120
|
+
export async function accept(ctx, input) {
|
|
121
|
+
if (!input.milestoneId)
|
|
122
|
+
refuse("INPUT", "milestoneId is required");
|
|
123
|
+
const key = await ctx.unlock();
|
|
124
|
+
const me = key.address;
|
|
125
|
+
let arbiters = [];
|
|
126
|
+
try {
|
|
127
|
+
arbiters = loadPolicy(ctx.home, key.macKey, me).arbiters;
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
// A payee without a policy accepts only escrows without an arbiter; a TAMPERED policy is never read as empty.
|
|
131
|
+
if (!(e instanceof Error && /POLICY_MISSING/.test(e.message)))
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
const offer = (await ctx.remote.acceptance(input.milestoneId));
|
|
135
|
+
await verifyEndpoint(ctx.rpc, ctx.net);
|
|
136
|
+
const head = await latestBlock({ rpc: ctx.rpc, net: ctx.net });
|
|
137
|
+
const msg = checkAcceptance(offer, {
|
|
138
|
+
me,
|
|
139
|
+
net: ctx.net,
|
|
140
|
+
chainNowSec: head.timestamp,
|
|
141
|
+
arbiters,
|
|
142
|
+
minAmountWei: input.minAmountXrp ? parseEther(input.minAmountXrp) : null,
|
|
143
|
+
});
|
|
144
|
+
const signature = await key.signTypedData(acceptanceDomain(ctx.net.chainId, ctx.net.address), ACCEPTANCE_TYPES, { ...msg });
|
|
145
|
+
const answer = await ctx.remote.submitAcceptance(offer.offerId, signature);
|
|
146
|
+
new Book(ctx.home, key.macKey).add({
|
|
147
|
+
escrowId: msg.escrowId,
|
|
148
|
+
role: "payee",
|
|
149
|
+
milestoneId: input.milestoneId,
|
|
150
|
+
payer: offer.payer,
|
|
151
|
+
payee: me,
|
|
152
|
+
amountWei: msg.amount.toString(),
|
|
153
|
+
chainId: ctx.net.chainId,
|
|
154
|
+
contract: ctx.net.address,
|
|
155
|
+
at: head.timestamp,
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
accepted: true,
|
|
159
|
+
escrowId: msg.escrowId,
|
|
160
|
+
payer: getAddress(offer.payer),
|
|
161
|
+
amountWei: msg.amount.toString(),
|
|
162
|
+
acceptBy: msg.acceptBy.toString(),
|
|
163
|
+
deadline: msg.deadline.toString(),
|
|
164
|
+
reviewPeriod: msg.reviewPeriod,
|
|
165
|
+
arbiter: msg.arbiter,
|
|
166
|
+
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.",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { PER_ESCROW_ACTIONS } from "./calldata.js";
|
|
2
|
+
import type { SignerAction } from "./calldata.js";
|
|
3
|
+
import { RemoteError } from "./remote.js";
|
|
4
|
+
import type { V3EscrowAction } from "./generated/v3-actions.js";
|
|
5
|
+
import type { SignerContext } from "./context.js";
|
|
6
|
+
export type ActionResult = {
|
|
7
|
+
action: SignerAction;
|
|
8
|
+
escrowId: string;
|
|
9
|
+
txHash: string;
|
|
10
|
+
sent: boolean;
|
|
11
|
+
reported: boolean;
|
|
12
|
+
from: string;
|
|
13
|
+
notes: string[];
|
|
14
|
+
[k: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type FundInput = {
|
|
17
|
+
milestoneId: string;
|
|
18
|
+
maxAmountXrp?: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function fund(ctx: SignerContext, input: FundInput): Promise<ActionResult>;
|
|
21
|
+
export type EvidenceInput = {
|
|
22
|
+
files?: string[];
|
|
23
|
+
texts?: string[];
|
|
24
|
+
github?: {
|
|
25
|
+
commitSha: string;
|
|
26
|
+
diffSha256: string;
|
|
27
|
+
}[];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The manifest hash of evidence the payee holds locally, computed HERE with the
|
|
31
|
+
* one manifest encoding. A submission is what ONE evidence-door request sealed —
|
|
32
|
+
* today one file, one text or one GitHub item — so the hash of several items only
|
|
33
|
+
* matches a submission that sealed exactly those items. It is never trusted on
|
|
34
|
+
* its own: `submitClaim` sends it only after cascrow's prepare confirms it is a
|
|
35
|
+
* sealed submission of the milestone (post-review G2).
|
|
36
|
+
*/
|
|
37
|
+
export declare function localProofHash(milestoneId: string, ev: EvidenceInput): string;
|
|
38
|
+
export type EscrowActionInput = {
|
|
39
|
+
escrowId?: string;
|
|
40
|
+
milestoneId?: string;
|
|
41
|
+
/** object: the objection reason; its keccak256 goes on chain. */
|
|
42
|
+
reason?: string;
|
|
43
|
+
/** extendDeadline: unix seconds. */
|
|
44
|
+
newDeadline?: string | number;
|
|
45
|
+
/** submitClaim: the manifestHash the evidence door returned for the sealed submission. */
|
|
46
|
+
proofHash?: string;
|
|
47
|
+
/** submitClaim: evidence to hash locally instead (must equal a sealed submission). */
|
|
48
|
+
evidence?: EvidenceInput;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The fields of a refusal body a recipient can act on, as one line. The same
|
|
52
|
+
* fields every cascrow client appends (post-review G5).
|
|
53
|
+
*/
|
|
54
|
+
export declare function refusalDetail(body: Record<string, unknown>): string;
|
|
55
|
+
/** True when prepare's failure is cascrow saying no (a verdict), not cascrow being unavailable. */
|
|
56
|
+
export declare function isPrepareRefusal(e: unknown): e is RemoteError;
|
|
57
|
+
export declare function escrowAction(ctx: SignerContext, action: V3EscrowAction, input: EscrowActionInput): Promise<ActionResult>;
|
|
58
|
+
/** withdraw(own account, native XRP): pays this key's credit to this key. No escrow, no cascrow. */
|
|
59
|
+
export declare function withdraw(ctx: SignerContext): Promise<ActionResult>;
|
|
60
|
+
export { PER_ESCROW_ACTIONS };
|