movehat 0.2.0 → 0.2.2
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 +132 -279
- package/dist/__tests__/deployContract.test.js +56 -47
- package/dist/__tests__/deployContract.test.js.map +1 -1
- package/dist/__tests__/exports.test.d.ts +2 -0
- package/dist/__tests__/exports.test.d.ts.map +1 -0
- package/dist/__tests__/exports.test.js +30 -0
- package/dist/__tests__/exports.test.js.map +1 -0
- package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts +4 -3
- package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts.map +1 -1
- package/dist/__tests__/fixtures/sigint-deploy-harness.js +8 -7
- package/dist/__tests__/fixtures/sigint-deploy-harness.js.map +1 -1
- package/dist/__tests__/fork/api.test.js +7 -2
- package/dist/__tests__/fork/api.test.js.map +1 -1
- package/dist/__tests__/fork/api.timeout.test.d.ts +2 -0
- package/dist/__tests__/fork/api.timeout.test.d.ts.map +1 -0
- package/dist/__tests__/fork/api.timeout.test.js +98 -0
- package/dist/__tests__/fork/api.timeout.test.js.map +1 -0
- package/dist/__tests__/harness/Harness.proxy.test.js +7 -11
- package/dist/__tests__/harness/Harness.proxy.test.js.map +1 -1
- package/dist/__tests__/harness/codeObject.deploy.test.js +1 -1
- package/dist/__tests__/harness/codeObject.deploy.test.js.map +1 -1
- package/dist/__tests__/harness/view.test.js +3 -3
- package/dist/commands/__tests__/compile.toml-mutation.test.d.ts +2 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.d.ts.map +1 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.js +69 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.js.map +1 -0
- package/dist/commands/__tests__/init.test.js +73 -11
- package/dist/commands/__tests__/init.test.js.map +1 -1
- package/dist/commands/__tests__/run.test.js +3 -3
- package/dist/commands/__tests__/run.test.js.map +1 -1
- package/dist/commands/init.d.ts +22 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +55 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/core/AccountManager.d.ts +0 -3
- package/dist/core/AccountManager.d.ts.map +1 -1
- package/dist/core/AccountManager.js +14 -7
- package/dist/core/AccountManager.js.map +1 -1
- package/dist/core/Publisher.d.ts +0 -5
- package/dist/core/Publisher.d.ts.map +1 -1
- package/dist/core/Publisher.js +52 -76
- package/dist/core/Publisher.js.map +1 -1
- package/dist/core/__tests__/AccountManager.global-state.test.d.ts +2 -0
- package/dist/core/__tests__/AccountManager.global-state.test.d.ts.map +1 -0
- package/dist/core/__tests__/AccountManager.global-state.test.js +69 -0
- package/dist/core/__tests__/AccountManager.global-state.test.js.map +1 -0
- package/dist/core/__tests__/movementProfile.test.d.ts +2 -0
- package/dist/core/__tests__/movementProfile.test.d.ts.map +1 -0
- package/dist/core/__tests__/movementProfile.test.js +112 -0
- package/dist/core/__tests__/movementProfile.test.js.map +1 -0
- package/dist/core/config.js +6 -5
- package/dist/core/config.js.map +1 -1
- package/dist/core/contract.d.ts +0 -3
- package/dist/core/contract.d.ts.map +1 -1
- package/dist/core/contract.js +0 -3
- package/dist/core/contract.js.map +1 -1
- package/dist/core/deployments.d.ts +0 -6
- package/dist/core/deployments.d.ts.map +1 -1
- package/dist/core/deployments.js +0 -12
- package/dist/core/deployments.js.map +1 -1
- package/dist/core/movementProfile.d.ts +55 -22
- package/dist/core/movementProfile.d.ts.map +1 -1
- package/dist/core/movementProfile.js +77 -99
- package/dist/core/movementProfile.js.map +1 -1
- package/dist/fork/__tests__/manager.test.js +1 -1
- package/dist/fork/__tests__/server.cors.test.d.ts +2 -0
- package/dist/fork/__tests__/server.cors.test.d.ts.map +1 -0
- package/dist/fork/__tests__/server.cors.test.js +79 -0
- package/dist/fork/__tests__/server.cors.test.js.map +1 -0
- package/dist/fork/api.d.ts +9 -1
- package/dist/fork/api.d.ts.map +1 -1
- package/dist/fork/api.js +37 -7
- package/dist/fork/api.js.map +1 -1
- package/dist/fork/manager.d.ts +1 -21
- package/dist/fork/manager.d.ts.map +1 -1
- package/dist/fork/manager.js +1 -41
- package/dist/fork/manager.js.map +1 -1
- package/dist/fork/server.d.ts +20 -1
- package/dist/fork/server.d.ts.map +1 -1
- package/dist/fork/server.js +19 -9
- package/dist/fork/server.js.map +1 -1
- package/dist/fork/test.d.ts +0 -1
- package/dist/fork/test.d.ts.map +1 -1
- package/dist/fork/test.js.map +1 -1
- package/dist/harness/Harness.d.ts +11 -13
- package/dist/harness/Harness.d.ts.map +1 -1
- package/dist/harness/Harness.js +13 -13
- package/dist/harness/Harness.js.map +1 -1
- package/dist/harness/codeObject.d.ts.map +1 -1
- package/dist/harness/codeObject.js +31 -38
- package/dist/harness/codeObject.js.map +1 -1
- package/dist/harness/script.d.ts +3 -3
- package/dist/harness/script.d.ts.map +1 -1
- package/dist/harness/script.js +33 -29
- package/dist/harness/script.js.map +1 -1
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts +2 -0
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts.map +1 -0
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js +172 -0
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js.map +1 -0
- package/dist/helpers/setupLocalTesting.d.ts +1 -2
- package/dist/helpers/setupLocalTesting.d.ts.map +1 -1
- package/dist/helpers/setupLocalTesting.js +28 -2
- package/dist/helpers/setupLocalTesting.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/node/LocalNodeManager.d.ts +8 -0
- package/dist/node/LocalNodeManager.d.ts.map +1 -1
- package/dist/node/LocalNodeManager.js +10 -1
- package/dist/node/LocalNodeManager.js.map +1 -1
- package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts +2 -0
- package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts.map +1 -0
- package/dist/node/__tests__/LocalNodeManager.api-port.test.js +55 -0
- package/dist/node/__tests__/LocalNodeManager.api-port.test.js.map +1 -0
- package/dist/node/__tests__/LocalNodeManager.test.js +4 -3
- package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1 -3
- package/dist/runtime.js.map +1 -1
- package/dist/templates/move/Move.toml +1 -1
- package/dist/templates/move/sources/Counter.move +31 -4
- package/dist/templates/scripts/deploy-counter.ts +11 -1
- package/dist/templates/tests/Counter.test.ts +2 -2
- package/dist/types/config.d.ts +8 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts +2 -0
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts.map +1 -0
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js +43 -0
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js.map +1 -0
- package/dist/utils/address.d.ts +0 -4
- package/dist/utils/address.d.ts.map +1 -1
- package/dist/utils/address.js +0 -4
- package/dist/utils/address.js.map +1 -1
- package/dist/utils/childProcessAdapter.d.ts +7 -0
- package/dist/utils/childProcessAdapter.d.ts.map +1 -1
- package/dist/utils/childProcessAdapter.js +23 -6
- package/dist/utils/childProcessAdapter.js.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/deployContract.test.ts +59 -50
- package/src/__tests__/exports.test.ts +32 -0
- package/src/__tests__/fixtures/sigint-deploy-harness.ts +8 -7
- package/src/__tests__/fork/api.test.ts +7 -2
- package/src/__tests__/fork/api.timeout.test.ts +150 -0
- package/src/__tests__/harness/Harness.proxy.test.ts +7 -11
- package/src/__tests__/harness/codeObject.deploy.test.ts +1 -1
- package/src/__tests__/harness/view.test.ts +3 -3
- package/src/commands/__tests__/compile.toml-mutation.test.ts +77 -0
- package/src/commands/__tests__/init.test.ts +96 -11
- package/src/commands/__tests__/run.test.ts +3 -3
- package/src/commands/init.ts +77 -6
- package/src/core/AccountManager.ts +18 -13
- package/src/core/Publisher.ts +58 -85
- package/src/core/__tests__/AccountManager.global-state.test.ts +83 -0
- package/src/core/__tests__/movementProfile.test.ts +131 -0
- package/src/core/config.ts +9 -5
- package/src/core/contract.ts +0 -3
- package/src/core/deployments.ts +0 -12
- package/src/core/movementProfile.ts +75 -127
- package/src/fork/__tests__/manager.test.ts +1 -1
- package/src/fork/__tests__/server.cors.test.ts +101 -0
- package/src/fork/api.ts +69 -10
- package/src/fork/manager.ts +1 -41
- package/src/fork/server.ts +38 -9
- package/src/fork/test.ts +0 -1
- package/src/harness/Harness.ts +16 -13
- package/src/harness/codeObject.ts +38 -48
- package/src/harness/script.ts +40 -39
- package/src/helpers/__tests__/setupLocalTesting.fork-network.test.ts +212 -0
- package/src/helpers/setupLocalTesting.ts +37 -4
- package/src/index.ts +9 -2
- package/src/node/LocalNodeManager.ts +24 -2
- package/src/node/__tests__/LocalNodeManager.api-port.test.ts +62 -0
- package/src/node/__tests__/LocalNodeManager.test.ts +5 -4
- package/src/runtime.ts +1 -3
- package/src/templates/move/Move.toml +1 -1
- package/src/templates/move/sources/Counter.move +31 -4
- package/src/templates/scripts/deploy-counter.ts +11 -1
- package/src/templates/tests/Counter.test.ts +2 -2
- package/src/types/config.ts +8 -1
- package/src/types/runtime.ts +2 -2
- package/src/utils/__tests__/childProcessAdapter.maxBuffer.test.ts +51 -0
- package/src/utils/address.ts +0 -4
- package/src/utils/childProcessAdapter.ts +35 -6
|
@@ -1,154 +1,102 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
renameSync,
|
|
7
|
-
unlinkSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from "fs";
|
|
10
|
-
import { readFile } from "fs/promises";
|
|
11
|
-
import { dirname } from "path";
|
|
1
|
+
import { chmodSync, existsSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
12
4
|
import { randomUUID } from "crypto";
|
|
13
|
-
import * as yaml from "js-yaml";
|
|
14
5
|
|
|
15
6
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
7
|
+
* Per-deploy private-key file management and SIGINT/SIGTERM cleanup
|
|
8
|
+
* infrastructure shared across the Movement CLI invocations (`move
|
|
9
|
+
* publish`, `move deploy-object`, `move upgrade-object`, `move
|
|
10
|
+
* run-script`).
|
|
18
11
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
12
|
+
* **Why a temp key file rather than the CLI's profile yaml?** The
|
|
13
|
+
* Movement CLI's `--profile <name>` flag requires a config.yaml to
|
|
14
|
+
* exist in the working directory (older Movement CLI variants look
|
|
15
|
+
* for `<cwd>/.aptos/config.yaml`; newer variants look for
|
|
16
|
+
* `<cwd>/.movement/config.yaml`). On fresh user installs neither
|
|
17
|
+
* directory exists, and the CLI errors out before it would otherwise
|
|
18
|
+
* fall back to `~/.aptos/config.yaml`. Writing to a temp file and
|
|
19
|
+
* passing `--private-key-file <path> --sender-account <addr>`
|
|
20
|
+
* directly avoids the entire profile-yaml lookup chain — no CWD
|
|
21
|
+
* dependency, no CLI-variant dependency.
|
|
22
|
+
*
|
|
23
|
+
* The same SIGINT-safe cleanup pattern applies as the old profile
|
|
24
|
+
* flow: the temp key file persists private key material on disk and
|
|
25
|
+
* MUST be removed before process exit even on abnormal termination.
|
|
23
26
|
*
|
|
24
27
|
* @internal — not exported from `src/index.ts`.
|
|
25
28
|
*/
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* Write the private key to a `mode 0o600` file in the system temp
|
|
32
|
+
* directory and return the path. The filename is UUID-suffixed so
|
|
33
|
+
* concurrent deploys don't collide.
|
|
34
|
+
*
|
|
35
|
+
* The key string is written verbatim — callers should format to
|
|
36
|
+
* AIP-80 before calling (see `PrivateKey.formatPrivateKey` from
|
|
37
|
+
* `@aptos-labs/ts-sdk`).
|
|
31
38
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
yamlLock = next.catch(() => {}); // swallow on the shared chain; caller still gets the original
|
|
42
|
-
return next;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ProfileData {
|
|
46
|
-
private_key: string;
|
|
47
|
-
public_key: string;
|
|
48
|
-
account: string;
|
|
49
|
-
rest_url: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface AptosConfigYaml {
|
|
53
|
-
profiles?: Record<string, ProfileData>;
|
|
54
|
-
[key: string]: unknown;
|
|
39
|
+
export function writeTempKeyFile(privateKey: string): string {
|
|
40
|
+
const path = join(tmpdir(), `movehat-key-${randomUUID()}`);
|
|
41
|
+
// mode in the open() call may be filtered by the process umask
|
|
42
|
+
// (typically 0o022 → resulting perms 0o644). chmod after write
|
|
43
|
+
// is defense in depth so the file can never be observable as
|
|
44
|
+
// group-/world-readable while it carries the private key.
|
|
45
|
+
writeFileSync(path, privateKey, { mode: 0o600 });
|
|
46
|
+
chmodSync(path, 0o600);
|
|
47
|
+
return path;
|
|
55
48
|
}
|
|
56
49
|
|
|
57
50
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
renameSync(tmpPath, path);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Add the deploy's profile to ~/.aptos/config.yaml. Creates the file if absent. */
|
|
71
|
-
export async function addProfile(
|
|
72
|
-
configPath: string,
|
|
73
|
-
name: string,
|
|
74
|
-
data: ProfileData
|
|
75
|
-
): Promise<void> {
|
|
76
|
-
const configDir = dirname(configPath);
|
|
77
|
-
if (!existsSync(configDir)) {
|
|
78
|
-
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
79
|
-
}
|
|
80
|
-
let yamlObj: AptosConfigYaml = {};
|
|
81
|
-
if (existsSync(configPath)) {
|
|
82
|
-
const raw = await readFile(configPath, "utf-8");
|
|
83
|
-
yamlObj = (yaml.load(raw) as AptosConfigYaml) || {};
|
|
84
|
-
}
|
|
85
|
-
if (!yamlObj.profiles) yamlObj.profiles = {};
|
|
86
|
-
yamlObj.profiles[name] = data;
|
|
87
|
-
atomicWriteYaml(configPath, yaml.dump(yamlObj));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Remove the deploy's profile from ~/.aptos/config.yaml. Idempotent —
|
|
92
|
-
* a missing file or missing profile is a no-op. If removal leaves the
|
|
93
|
-
* yaml with only an empty `profiles:` block, the whole file is unlinked
|
|
94
|
-
* to preserve the "didn't exist before" semantic for the first-ever deploy.
|
|
51
|
+
* Sync unlink for SIGINT/SIGTERM handlers — never throws, never logs.
|
|
52
|
+
* The event loop is dead by the time this runs; observability isn't
|
|
53
|
+
* possible. Worst case: a stale 0o600 temp file in `os.tmpdir()`
|
|
54
|
+
* that the OS reaps eventually.
|
|
55
|
+
*
|
|
56
|
+
* **Do not use this from a normal `finally` cleanup path** — failures
|
|
57
|
+
* become invisible there, hiding the case where a private-key temp
|
|
58
|
+
* file persists on disk after a deploy. Use {@link removeKeyFile}
|
|
59
|
+
* instead for those paths.
|
|
95
60
|
*/
|
|
96
|
-
export
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
delete yamlObj.profiles[name];
|
|
102
|
-
|
|
103
|
-
const profilesEmpty = Object.keys(yamlObj.profiles).length === 0;
|
|
104
|
-
const onlyProfilesKey =
|
|
105
|
-
Object.keys(yamlObj).length === 1 && "profiles" in yamlObj;
|
|
106
|
-
if (profilesEmpty && onlyProfilesKey) {
|
|
107
|
-
// We created this file fresh; remove it.
|
|
108
|
-
try {
|
|
109
|
-
unlinkSync(configPath);
|
|
110
|
-
} catch {
|
|
111
|
-
// best-effort
|
|
112
|
-
}
|
|
113
|
-
return;
|
|
61
|
+
export function removeKeyFileSyncBestEffort(path: string): void {
|
|
62
|
+
try {
|
|
63
|
+
unlinkSync(path);
|
|
64
|
+
} catch {
|
|
65
|
+
// event loop dead — best-effort
|
|
114
66
|
}
|
|
115
|
-
atomicWriteYaml(configPath, yaml.dump(yamlObj));
|
|
116
67
|
}
|
|
117
68
|
|
|
118
69
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
70
|
+
* Sync unlink for the normal cleanup path (a `finally` block after the
|
|
71
|
+
* Movement CLI invocation returns). Returns `null` on success — either
|
|
72
|
+
* the file was removed, or it was already gone (ENOENT is treated as
|
|
73
|
+
* benign success). Returns an `Error` only when the file **still
|
|
74
|
+
* exists on disk** after the unlink attempt failed (EPERM, EACCES,
|
|
75
|
+
* EBUSY, EISDIR if the path collided with a directory, etc.).
|
|
76
|
+
*
|
|
77
|
+
* Callers SHOULD `logger.warning` when a non-null Error is returned —
|
|
78
|
+
* a private-key temp file would otherwise persist silently.
|
|
124
79
|
*/
|
|
125
|
-
export function
|
|
80
|
+
export function removeKeyFile(path: string): Error | null {
|
|
126
81
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
atomicWriteYaml(configPath, yaml.dump(yamlObj));
|
|
141
|
-
} catch {
|
|
142
|
-
// Signal handlers should never throw — swallow and exit. Better to
|
|
143
|
-
// leave a stale profile (recoverable by re-running the deploy) than
|
|
144
|
-
// to crash the parent process mid-shutdown.
|
|
82
|
+
unlinkSync(path);
|
|
83
|
+
return null;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
86
|
+
if (code === "ENOENT") return null;
|
|
87
|
+
// The unlink call failed but verify the file actually still exists
|
|
88
|
+
// before declaring this preocupante — some races (parallel cleanup,
|
|
89
|
+
// tmpdir reaper) can race with us and the file may already be gone
|
|
90
|
+
// despite the syscall reporting an unexpected error.
|
|
91
|
+
if (!existsSync(path)) return null;
|
|
92
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
145
93
|
}
|
|
146
94
|
}
|
|
147
95
|
|
|
148
96
|
/**
|
|
149
97
|
* Process-level signal handling. A single registered handler iterates
|
|
150
|
-
* the per-deploy cleanup callbacks.
|
|
151
|
-
* concurrent deploys share the same parent
|
|
98
|
+
* the per-deploy cleanup callbacks. Installed once per process because
|
|
99
|
+
* multiple concurrent deploys share the same parent — installing per
|
|
152
100
|
* deploy would re-add the listener and exceed Node's max-listeners
|
|
153
101
|
* warning threshold under heavy parallelism.
|
|
154
102
|
*/
|
|
@@ -159,7 +107,7 @@ export function ensureSignalHandler(): void {
|
|
|
159
107
|
if (signalHandlerInstalled) return;
|
|
160
108
|
signalHandlerInstalled = true;
|
|
161
109
|
const handler = (sig: NodeJS.Signals) => {
|
|
162
|
-
// Synchronous cleanup of every active deploy's
|
|
110
|
+
// Synchronous cleanup of every active deploy's resources.
|
|
163
111
|
for (const cb of [...cleanupCallbacks]) {
|
|
164
112
|
try {
|
|
165
113
|
cb();
|
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Tests for `ForkManager
|
|
7
|
+
* Tests for `ForkManager`. Strategy:
|
|
8
8
|
* - `vi.mock` the upstream `MovementApiClient` (network layer) so
|
|
9
9
|
* tests stay offline and deterministic.
|
|
10
10
|
* - Use a REAL `ForkStorage` against a tmpdir (storage is already
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { AddressInfo } from "node:net";
|
|
6
|
+
|
|
7
|
+
import { ForkServer } from "../server.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* F2 — ForkServer must not advertise `Access-Control-Allow-Origin: *`
|
|
11
|
+
* by default. Cached fork state may include account resources fetched
|
|
12
|
+
* from authenticated upstream nodes; any web page open in the dev's
|
|
13
|
+
* browser should not be able to read it through the loopback listener.
|
|
14
|
+
*
|
|
15
|
+
* Default contract:
|
|
16
|
+
* - No CORS header emitted unless the caller opts in via
|
|
17
|
+
* `corsAllowOrigins`.
|
|
18
|
+
* - When opted in, only requests whose `Origin` is in the allowlist
|
|
19
|
+
* receive a matching `Access-Control-Allow-Origin` (echo of origin,
|
|
20
|
+
* not `*`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
function makeForkDir(): string {
|
|
24
|
+
const dir = mkdtempSync(join(tmpdir(), "movehat-fork-cors-"));
|
|
25
|
+
mkdirSync(join(dir, "resources"), { recursive: true });
|
|
26
|
+
writeFileSync(
|
|
27
|
+
join(dir, "metadata.json"),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
network: "test",
|
|
30
|
+
nodeUrl: "http://example.invalid/v1",
|
|
31
|
+
chainId: 0,
|
|
32
|
+
ledgerVersion: "0",
|
|
33
|
+
timestamp: "0",
|
|
34
|
+
epoch: "0",
|
|
35
|
+
blockHeight: "0",
|
|
36
|
+
createdAt: new Date().toISOString(),
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
writeFileSync(join(dir, "accounts.json"), "{}");
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function boundPort(server: ForkServer): number {
|
|
44
|
+
const internal = (server as unknown as {
|
|
45
|
+
server: { address(): AddressInfo };
|
|
46
|
+
}).server;
|
|
47
|
+
const addr = internal.address();
|
|
48
|
+
return addr.port;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchFromServer(
|
|
52
|
+
port: number,
|
|
53
|
+
origin?: string
|
|
54
|
+
): Promise<Response> {
|
|
55
|
+
const headers: Record<string, string> = {};
|
|
56
|
+
if (origin !== undefined) headers.Origin = origin;
|
|
57
|
+
return fetch(`http://127.0.0.1:${port}/v1/`, { headers });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("F2 — ForkServer CORS is closed by default", () => {
|
|
61
|
+
let forkDir: string;
|
|
62
|
+
let server: ForkServer | null = null;
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
forkDir = makeForkDir();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
if (server) {
|
|
70
|
+
await server.stop();
|
|
71
|
+
server = null;
|
|
72
|
+
}
|
|
73
|
+
rmSync(forkDir, { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not emit Access-Control-Allow-Origin by default", async () => {
|
|
77
|
+
server = new ForkServer(forkDir, 0);
|
|
78
|
+
await server.start();
|
|
79
|
+
const port = boundPort(server);
|
|
80
|
+
|
|
81
|
+
const res = await fetchFromServer(port, "https://evil.example");
|
|
82
|
+
expect(res.status).toBe(200);
|
|
83
|
+
expect(res.headers.get("access-control-allow-origin")).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("echoes only allow-listed origins when corsAllowOrigins is set", async () => {
|
|
87
|
+
server = new ForkServer(forkDir, 0, "127.0.0.1", {
|
|
88
|
+
corsAllowOrigins: ["https://trusted.example"],
|
|
89
|
+
});
|
|
90
|
+
await server.start();
|
|
91
|
+
const port = boundPort(server);
|
|
92
|
+
|
|
93
|
+
const allowed = await fetchFromServer(port, "https://trusted.example");
|
|
94
|
+
expect(allowed.headers.get("access-control-allow-origin")).toBe(
|
|
95
|
+
"https://trusted.example"
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const denied = await fetchFromServer(port, "https://evil.example");
|
|
99
|
+
expect(denied.headers.get("access-control-allow-origin")).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
package/src/fork/api.ts
CHANGED
|
@@ -4,6 +4,16 @@ import { URL } from 'url';
|
|
|
4
4
|
import type { LedgerInfo, AccountData, AccountResource } from '../types/fork.js';
|
|
5
5
|
import { normalizeAddressShort } from '../utils/address.js';
|
|
6
6
|
|
|
7
|
+
export interface MovementApiClientOptions {
|
|
8
|
+
/** Abort the request after this many ms (default: 30_000). */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
/** Reject responses larger than this many bytes (default: 16 MiB). */
|
|
11
|
+
maxBytes?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
+
const DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
16
|
+
|
|
7
17
|
/**
|
|
8
18
|
* Client for interacting with Movement L1 JSON API.
|
|
9
19
|
*
|
|
@@ -15,8 +25,14 @@ import { normalizeAddressShort } from '../utils/address.js';
|
|
|
15
25
|
export class MovementApiClient {
|
|
16
26
|
private nodeUrl: string;
|
|
17
27
|
private readonly apiKey?: string;
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
private readonly timeoutMs: number;
|
|
29
|
+
private readonly maxBytes: number;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
nodeUrl: string,
|
|
33
|
+
apiKey?: string,
|
|
34
|
+
options: MovementApiClientOptions = {}
|
|
35
|
+
) {
|
|
20
36
|
// Remove trailing slash
|
|
21
37
|
let normalized = nodeUrl.replace(/\/$/, '');
|
|
22
38
|
|
|
@@ -28,6 +44,8 @@ export class MovementApiClient {
|
|
|
28
44
|
|
|
29
45
|
this.nodeUrl = normalized;
|
|
30
46
|
if (apiKey !== undefined) this.apiKey = apiKey;
|
|
47
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
48
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
/**
|
|
@@ -52,31 +70,72 @@ export class MovementApiClient {
|
|
|
52
70
|
requestOptions.headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
53
71
|
}
|
|
54
72
|
|
|
73
|
+
const timeoutMs = this.timeoutMs;
|
|
74
|
+
const maxBytes = this.maxBytes;
|
|
75
|
+
|
|
55
76
|
return new Promise((resolve, reject) => {
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
let settled = false;
|
|
78
|
+
const settle = (fn: () => void) => {
|
|
79
|
+
if (settled) return;
|
|
80
|
+
settled = true;
|
|
81
|
+
fn();
|
|
82
|
+
};
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
const req = client.get(fullUrl, requestOptions, (res) => {
|
|
85
|
+
const chunks: Buffer[] = [];
|
|
86
|
+
let totalBytes = 0;
|
|
87
|
+
|
|
88
|
+
res.on('data', (chunk: Buffer | string) => {
|
|
89
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
90
|
+
totalBytes += buf.length;
|
|
91
|
+
if (totalBytes > maxBytes) {
|
|
92
|
+
req.destroy();
|
|
93
|
+
settle(() =>
|
|
94
|
+
reject(
|
|
95
|
+
new Error(
|
|
96
|
+
`Response exceeded maxBytes (${maxBytes}); ${totalBytes} bytes received before abort`
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
chunks.push(buf);
|
|
61
103
|
});
|
|
62
104
|
|
|
63
105
|
res.on('end', () => {
|
|
106
|
+
if (settled) return;
|
|
107
|
+
const data = Buffer.concat(chunks).toString('utf8');
|
|
64
108
|
if (res.statusCode !== 200) {
|
|
65
|
-
|
|
109
|
+
settle(() =>
|
|
110
|
+
reject(
|
|
111
|
+
new Error(
|
|
112
|
+
`API request failed with status ${res.statusCode}: ${data}`
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
);
|
|
66
116
|
return;
|
|
67
117
|
}
|
|
68
118
|
|
|
69
119
|
try {
|
|
70
120
|
const parsed = JSON.parse(data);
|
|
71
|
-
resolve(parsed);
|
|
121
|
+
settle(() => resolve(parsed));
|
|
72
122
|
} catch (err) {
|
|
73
|
-
|
|
123
|
+
settle(() =>
|
|
124
|
+
reject(new Error(`Failed to parse JSON response: ${err}`))
|
|
125
|
+
);
|
|
74
126
|
}
|
|
75
127
|
});
|
|
76
128
|
});
|
|
77
129
|
|
|
130
|
+
req.setTimeout(timeoutMs, () => {
|
|
131
|
+
req.destroy();
|
|
132
|
+
settle(() =>
|
|
133
|
+
reject(new Error(`API request timed out after ${timeoutMs}ms`))
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
78
137
|
req.on('error', (err) => {
|
|
79
|
-
reject(new Error(`API request failed: ${err.message}`));
|
|
138
|
+
settle(() => reject(new Error(`API request failed: ${err.message}`)));
|
|
80
139
|
});
|
|
81
140
|
|
|
82
141
|
req.end();
|
package/src/fork/manager.ts
CHANGED
|
@@ -70,16 +70,12 @@ export class ForkManager {
|
|
|
70
70
|
): Promise<void> {
|
|
71
71
|
if (apiKey !== undefined) this.apiKey = apiKey;
|
|
72
72
|
|
|
73
|
-
// Create API client (with optional Authorization header)
|
|
74
73
|
this.apiClient = new MovementApiClient(nodeUrl, this.apiKey);
|
|
75
74
|
|
|
76
|
-
// Fetch network info
|
|
77
75
|
const ledgerInfo = await this.apiClient.getLedgerInfo();
|
|
78
76
|
|
|
79
|
-
// Create fork structure
|
|
80
77
|
this.storage.initialize();
|
|
81
78
|
|
|
82
|
-
// Save metadata
|
|
83
79
|
this.metadata = {
|
|
84
80
|
network: networkName,
|
|
85
81
|
nodeUrl,
|
|
@@ -110,9 +106,6 @@ export class ForkManager {
|
|
|
110
106
|
this.apiClient = new MovementApiClient(this.metadata.nodeUrl, this.apiKey);
|
|
111
107
|
}
|
|
112
108
|
|
|
113
|
-
/**
|
|
114
|
-
* Get fork metadata
|
|
115
|
-
*/
|
|
116
109
|
getMetadata(): ForkMetadata {
|
|
117
110
|
if (!this.metadata) {
|
|
118
111
|
this.metadata = this.storage.loadMetadata();
|
|
@@ -120,18 +113,12 @@ export class ForkManager {
|
|
|
120
113
|
return this.metadata;
|
|
121
114
|
}
|
|
122
115
|
|
|
123
|
-
/**
|
|
124
|
-
* Get account state (with lazy loading)
|
|
125
|
-
*/
|
|
126
116
|
async getAccount(address: string): Promise<AccountState> {
|
|
127
|
-
// Normalize address
|
|
128
117
|
const normalizedAddress = normalizeAddress(address);
|
|
129
118
|
|
|
130
|
-
// Check cache first
|
|
131
119
|
let accountState = this.storage.getAccount(normalizedAddress);
|
|
132
120
|
|
|
133
121
|
if (!accountState) {
|
|
134
|
-
// Fetch from network
|
|
135
122
|
if (!this.apiClient) {
|
|
136
123
|
throw new Error('Fork not initialized. Call initialize() or load() first.');
|
|
137
124
|
}
|
|
@@ -144,7 +131,6 @@ export class ForkManager {
|
|
|
144
131
|
authenticationKey: accountData.authentication_key,
|
|
145
132
|
};
|
|
146
133
|
|
|
147
|
-
// Cache it
|
|
148
134
|
this.storage.saveAccount(normalizedAddress, accountState);
|
|
149
135
|
console.log(` ✓ Cached account ${normalizedAddress}`);
|
|
150
136
|
}
|
|
@@ -152,17 +138,12 @@ export class ForkManager {
|
|
|
152
138
|
return accountState;
|
|
153
139
|
}
|
|
154
140
|
|
|
155
|
-
/**
|
|
156
|
-
* Get a specific resource (with lazy loading)
|
|
157
|
-
*/
|
|
158
141
|
async getResource(address: string, resourceType: string): Promise<any> {
|
|
159
142
|
const normalizedAddress = normalizeAddress(address);
|
|
160
143
|
|
|
161
|
-
// Check cache first
|
|
162
144
|
let resource = this.storage.getResource(normalizedAddress, resourceType);
|
|
163
145
|
|
|
164
146
|
if (!resource) {
|
|
165
|
-
// Fetch from network
|
|
166
147
|
if (!this.apiClient) {
|
|
167
148
|
throw new Error('Fork not initialized. Call initialize() or load() first.');
|
|
168
149
|
}
|
|
@@ -173,7 +154,6 @@ export class ForkManager {
|
|
|
173
154
|
const resourceData = await this.apiClient.getAccountResource(normalizedAddress, resourceType);
|
|
174
155
|
resource = resourceData.data;
|
|
175
156
|
|
|
176
|
-
// Cache it
|
|
177
157
|
this.storage.saveResource(normalizedAddress, resourceType, resource);
|
|
178
158
|
console.log(` ✓ Cached resource ${resourceType}`);
|
|
179
159
|
} catch (error) {
|
|
@@ -188,16 +168,11 @@ export class ForkManager {
|
|
|
188
168
|
return resource;
|
|
189
169
|
}
|
|
190
170
|
|
|
191
|
-
/**
|
|
192
|
-
* Get all resources for an account (with lazy loading)
|
|
193
|
-
*/
|
|
194
171
|
async getAllResources(address: string): Promise<Record<string, any>> {
|
|
195
172
|
const normalizedAddress = normalizeAddress(address);
|
|
196
173
|
|
|
197
|
-
// Check if we have any cached resources
|
|
198
174
|
let resources = this.storage.getAllResources(normalizedAddress);
|
|
199
175
|
|
|
200
|
-
// If no cached resources, fetch all from network
|
|
201
176
|
if (Object.keys(resources).length === 0) {
|
|
202
177
|
if (!this.apiClient) {
|
|
203
178
|
throw new Error('Fork not initialized. Call initialize() or load() first.');
|
|
@@ -211,7 +186,6 @@ export class ForkManager {
|
|
|
211
186
|
resources[resource.type] = resource.data;
|
|
212
187
|
}
|
|
213
188
|
|
|
214
|
-
// Cache them
|
|
215
189
|
this.storage.saveAllResources(normalizedAddress, resources);
|
|
216
190
|
console.log(` ✓ Cached ${Object.keys(resources).length} resources`);
|
|
217
191
|
}
|
|
@@ -219,18 +193,13 @@ export class ForkManager {
|
|
|
219
193
|
return resources;
|
|
220
194
|
}
|
|
221
195
|
|
|
222
|
-
/**
|
|
223
|
-
* Set a resource value (for testing/mocking)
|
|
224
|
-
*/
|
|
225
196
|
async setResource(address: string, resourceType: string, data: unknown): Promise<void> {
|
|
226
197
|
const normalizedAddress = normalizeAddress(address);
|
|
227
198
|
this.storage.saveResource(normalizedAddress, resourceType, data);
|
|
228
199
|
console.log(` ✓ Updated resource ${resourceType} for ${normalizedAddress}`);
|
|
229
200
|
}
|
|
230
201
|
|
|
231
|
-
/**
|
|
232
|
-
* Fund an account with coins (adds to existing balance)
|
|
233
|
-
*/
|
|
202
|
+
/** Adds to the existing balance rather than replacing it. */
|
|
234
203
|
async fundAccount(address: string, amount: number, coinType: string = '0x1::aptos_coin::AptosCoin'): Promise<void> {
|
|
235
204
|
const normalizedAddress = normalizeAddress(address);
|
|
236
205
|
const resourceType = `0x1::coin::CoinStore<${coinType}>`;
|
|
@@ -250,7 +219,6 @@ export class ForkManager {
|
|
|
250
219
|
throw error;
|
|
251
220
|
}
|
|
252
221
|
|
|
253
|
-
// If doesn't exist, create new one
|
|
254
222
|
coinStore = {
|
|
255
223
|
coin: { value: '0' },
|
|
256
224
|
deposit_events: {
|
|
@@ -275,15 +243,12 @@ export class ForkManager {
|
|
|
275
243
|
};
|
|
276
244
|
}
|
|
277
245
|
|
|
278
|
-
// Add to existing balance (instead of replacing it)
|
|
279
246
|
const currentBalance = BigInt(coinStore.coin.value ?? '0');
|
|
280
247
|
const newBalance = currentBalance + BigInt(amount);
|
|
281
248
|
coinStore.coin.value = newBalance.toString();
|
|
282
249
|
|
|
283
|
-
// Save
|
|
284
250
|
await this.setResource(normalizedAddress, resourceType, coinStore);
|
|
285
251
|
|
|
286
|
-
// Also ensure account exists
|
|
287
252
|
let account = this.storage.getAccount(normalizedAddress);
|
|
288
253
|
if (!account) {
|
|
289
254
|
account = {
|
|
@@ -296,9 +261,6 @@ export class ForkManager {
|
|
|
296
261
|
console.log(` ✓ Funded ${normalizedAddress} with ${amount} coins`);
|
|
297
262
|
}
|
|
298
263
|
|
|
299
|
-
/**
|
|
300
|
-
* List all accounts in the fork
|
|
301
|
-
*/
|
|
302
264
|
listAccounts(): string[] {
|
|
303
265
|
return this.storage.listAccounts();
|
|
304
266
|
}
|
|
@@ -364,11 +326,9 @@ export class ForkManager {
|
|
|
364
326
|
async getOrCreateAccount(address: string): Promise<AccountState> {
|
|
365
327
|
const normalizedAddress = normalizeAddress(address);
|
|
366
328
|
|
|
367
|
-
// Try to get existing account
|
|
368
329
|
try {
|
|
369
330
|
return await this.getAccount(normalizedAddress);
|
|
370
331
|
} catch (error) {
|
|
371
|
-
// If account doesn't exist, create a minimal one
|
|
372
332
|
const newAccount: AccountState = {
|
|
373
333
|
sequenceNumber: '0',
|
|
374
334
|
authenticationKey: forkAuthKeyPlaceholder(normalizedAddress),
|