@starkscan/cli 0.1.0-alpha.1 → 0.1.0-beta.1
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 +85 -18
- package/artifacts/{v0.1.0-alpha.1 → v0.1.0-beta.1}/starkscan-cli-manifest.json +5 -5
- package/artifacts/{v0.1.0-alpha.1/starkscan-darwin-x86_64.tar.gz → v0.1.0-beta.1/starkscan-darwin-aarch64.tar.gz} +0 -0
- package/artifacts/v0.1.0-beta.1/starkscan-darwin-aarch64.tar.gz.sha256 +1 -0
- package/artifacts/v0.1.0-beta.1/starkscan-darwin-x86_64.tar.gz +0 -0
- package/artifacts/v0.1.0-beta.1/starkscan-darwin-x86_64.tar.gz.sha256 +1 -0
- package/artifacts/{v0.1.0-alpha.1/starkscan-linux-x86_64.tar.gz → v0.1.0-beta.1/starkscan-linux-aarch64.tar.gz} +0 -0
- package/artifacts/v0.1.0-beta.1/starkscan-linux-aarch64.tar.gz.sha256 +1 -0
- package/artifacts/v0.1.0-beta.1/starkscan-linux-x86_64.tar.gz +0 -0
- package/artifacts/v0.1.0-beta.1/starkscan-linux-x86_64.tar.gz.sha256 +1 -0
- package/bin/starkscan.js +20 -2
- package/package.json +18 -11
- package/scripts/installer.mjs +397 -78
- package/scripts/verify-bundled-artifacts.mjs +129 -18
- package/artifacts/v0.1.0-alpha.1/starkscan-darwin-aarch64.tar.gz +0 -0
- package/artifacts/v0.1.0-alpha.1/starkscan-darwin-aarch64.tar.gz.sha256 +0 -1
- package/artifacts/v0.1.0-alpha.1/starkscan-darwin-x86_64.tar.gz.sha256 +0 -1
- package/artifacts/v0.1.0-alpha.1/starkscan-linux-aarch64.tar.gz +0 -0
- package/artifacts/v0.1.0-alpha.1/starkscan-linux-aarch64.tar.gz.sha256 +0 -1
- package/artifacts/v0.1.0-alpha.1/starkscan-linux-x86_64.tar.gz.sha256 +0 -1
package/README.md
CHANGED
|
@@ -1,69 +1,136 @@
|
|
|
1
1
|
# @starkscan/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line launcher for Starkscan API, export, and agent workflows.
|
|
4
|
+
|
|
5
|
+
Status: public beta. Use a smoked exact version such as `0.1.0-beta.1` for unattended
|
|
6
|
+
agents or production services.
|
|
4
7
|
|
|
5
8
|
## Install
|
|
6
9
|
|
|
7
10
|
```bash
|
|
8
|
-
npm install -g @starkscan/cli@
|
|
9
|
-
starkscan
|
|
11
|
+
npm install -g @starkscan/cli@0.1.0-beta.1
|
|
12
|
+
starkscan init
|
|
10
13
|
```
|
|
11
14
|
|
|
12
15
|
One-off agent run:
|
|
13
16
|
|
|
14
17
|
```bash
|
|
15
|
-
npx @starkscan/cli@
|
|
18
|
+
npx -y @starkscan/cli@0.1.0-beta.1 init --agent --output-format json
|
|
16
19
|
```
|
|
17
20
|
|
|
18
21
|
For unattended agents or production services, pin a smoked exact version instead
|
|
19
|
-
of floating on `@
|
|
22
|
+
of floating on `@beta`.
|
|
23
|
+
|
|
24
|
+
## What it does
|
|
25
|
+
|
|
26
|
+
- Runs the prebuilt native `starkscan` binary without requiring a Rust toolchain.
|
|
27
|
+
- Verifies bundled native archives against release-manifest checksums.
|
|
28
|
+
- Calls the hosted Starkscan API with request IDs and route-class headers.
|
|
29
|
+
- Exposes shell-friendly output for agents, scripts, exports, and operator
|
|
30
|
+
smoke checks.
|
|
20
31
|
|
|
21
32
|
## Trust status
|
|
22
33
|
|
|
23
34
|
- npm: <https://www.npmjs.com/package/@starkscan/cli>
|
|
24
|
-
-
|
|
35
|
+
- public trust docs: <https://starkscan.co/docs/build/package-trust>
|
|
36
|
+
- machine-readable matrix: <https://starkscan.co/public-client-surface-matrix.json>
|
|
25
37
|
- Socket signal: <https://socket.dev/npm/package/@starkscan/cli>
|
|
26
38
|
|
|
27
39
|
Socket is an external package-risk signal, not a formal Starkscan security
|
|
28
40
|
certificate. Starkscan package promotion also requires bundled-artifact checksum
|
|
29
|
-
verification,
|
|
30
|
-
|
|
41
|
+
verification, manual passkey or Trusted Publishing/OIDC publish control, and
|
|
42
|
+
the launch gates documented at the public package trust page. The canonical
|
|
43
|
+
source repository is private, so public package trust links intentionally point
|
|
44
|
+
to Starkscan docs instead of GitHub.
|
|
31
45
|
|
|
32
46
|
The wrapper uses bundled native artifacts when they are present in the published package, verifies downloaded or bundled archives against checksums in the release manifest, rejects malformed archives, caches the native `starkscan` binary, and forwards all arguments to it. It does not require a Rust toolchain or repository access.
|
|
33
47
|
|
|
48
|
+
Agents can inspect the launcher decision without executing the native binary:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx -y @starkscan/cli@0.1.0-beta.1 --launcher-json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`--launcher-json` also works through `STARKSCAN_LAUNCHER_JSON=1` or
|
|
55
|
+
`STARKSCAN_CLI_LAUNCHER_JSON=1`. The JSON includes the package version,
|
|
56
|
+
resolved binary path, release tag, source (`bundled`, `download`, `bin-path`, or
|
|
57
|
+
`unknown` for legacy caches without valid sidecar metadata), cache-hit status, and
|
|
58
|
+
verification status. It may resolve and verify the binary first, but it does
|
|
59
|
+
not invoke the native `starkscan` executable.
|
|
60
|
+
|
|
34
61
|
MCP client setup uses the same package. `print-config` emits machine-readable
|
|
35
62
|
Codex and Claude Code snippets with environment-variable placeholders, not
|
|
36
63
|
secret values. `start` is the stable alias for serving the remote stdio bridge:
|
|
37
64
|
|
|
38
65
|
```bash
|
|
39
|
-
npx -y @starkscan/cli@
|
|
40
|
-
npx -y @starkscan/cli@
|
|
66
|
+
npx -y @starkscan/cli@0.1.0-beta.1 init --agent --output-format json
|
|
67
|
+
npx -y @starkscan/cli@0.1.0-beta.1 mcp print-config --transport remote
|
|
68
|
+
npx -y @starkscan/cli@0.1.0-beta.1 mcp start --transport remote
|
|
41
69
|
```
|
|
42
70
|
|
|
43
71
|
## Environment
|
|
44
72
|
|
|
45
73
|
```bash
|
|
46
|
-
# Use the URL prefix that directly serves `/v1/...`.
|
|
47
|
-
# Some public hosts may expose the API under `/api`; do not include `/v1`.
|
|
48
|
-
export STARKSCAN_BASE_URL="https://REPLACE_WITH_STARKSCAN_API_HOST"
|
|
49
74
|
export STARKSCAN_API_KEY="mzk_test_your_key_here"
|
|
50
75
|
export STARKSCAN_CHAIN="SN_MAIN"
|
|
76
|
+
# Optional: only set this for preview or self-hosted hosts.
|
|
77
|
+
# export STARKSCAN_BASE_URL="https://preview.example.com/api"
|
|
51
78
|
```
|
|
52
79
|
|
|
53
80
|
`STARKSCAN_*` is the public CLI environment contract. The launcher still accepts
|
|
54
81
|
the old internal variable names as hidden compatibility aliases during the beta
|
|
55
|
-
cutover. The CLI
|
|
82
|
+
cutover. The CLI defaults to `https://api.starkscan.co` and sends
|
|
83
|
+
`X-Starkscan-Api-Key` to the hosted API.
|
|
84
|
+
|
|
85
|
+
The native CLI forwards public API keys only to Starkscan HTTPS hosts or
|
|
86
|
+
localhost/loopback fixtures. When intentionally testing a private API host, pass
|
|
87
|
+
`--allow-untrusted-base-url` explicitly so keys are not replayed to an
|
|
88
|
+
unexpected endpoint by default.
|
|
89
|
+
|
|
90
|
+
Supported `STARKSCAN_CHAIN` values are `SN_MAIN` and `SN_SEPOLIA`. Global flags
|
|
91
|
+
can be placed before or after the subcommand, for example
|
|
92
|
+
`starkscan status --output-format json --pretty`.
|
|
93
|
+
|
|
94
|
+
For agent callers, `--output-format json` always keeps stdout parseable: success
|
|
95
|
+
prints `{ "ok": true, "data": ... }`, and failures before a result payload is
|
|
96
|
+
emitted print
|
|
97
|
+
`{ "ok": false, "error": { "code": "auth_error", "message": "HTTP 401: unauthorized", "exitCode": 3, "exitClass": "auth", "httpStatus": 401, "requestId": "starkscan-cli-..." } }`
|
|
98
|
+
before exiting non-zero. Diagnostic commands such as `doctor` can emit
|
|
99
|
+
`{ "ok": false, "data": ... }` with a non-zero exit. `stream-json` failures
|
|
100
|
+
print one compact `type: "error"` line. Text mode keeps human-readable errors on
|
|
101
|
+
stderr.
|
|
102
|
+
|
|
103
|
+
Transaction hashes, addresses, token addresses, and calldata values must be
|
|
104
|
+
`0x`-prefixed Starknet field elements. Contract-read selectors can be function
|
|
105
|
+
names such as `balanceOf` or selector felts. Malformed values fail locally before
|
|
106
|
+
any network request with exit code `2`; JSON mode emits `code: "usage_error"`.
|
|
107
|
+
In JSON output, address-shaped fields such as `address`, `fromAddress`,
|
|
108
|
+
`toAddress`, `contractAddress`, and `tokenAddress` are canonicalized to lowercase
|
|
109
|
+
`0x` plus 64 hex characters so agents can join rows reliably. Hashes, calldata,
|
|
110
|
+
selectors, and other felt values are not rewritten.
|
|
111
|
+
|
|
112
|
+
CLI exit codes are stable for agents: `0` success, `1` runtime, `2`
|
|
113
|
+
usage/bad input, `3` auth, `4` rate-limited, `5` timeout, and `6` not found.
|
|
114
|
+
|
|
115
|
+
## Minimal commands
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx -y @starkscan/cli@0.1.0-beta.1 init
|
|
119
|
+
npx -y @starkscan/cli@0.1.0-beta.1 status
|
|
120
|
+
npx -y @starkscan/cli@0.1.0-beta.1 doctor
|
|
121
|
+
npx -y @starkscan/cli@0.1.0-beta.1 mcp print-config --transport remote
|
|
122
|
+
```
|
|
56
123
|
|
|
57
124
|
## Release binding
|
|
58
125
|
|
|
59
126
|
By default, the package resolves to bundled artifacts under `artifacts/v<package-version>` when published with native binaries included. Maintainers can override the release source for tests or emergency rollback:
|
|
60
127
|
|
|
61
128
|
- `STARKSCAN_CLI_RELEASE_TAG`: release tag, or `latest`
|
|
62
|
-
- `STARKSCAN_CLI_BUNDLED_ARTIFACT_DIR`: maintainer/test override for the bundled artifact root. Use an absolute path in CI, or a cwd-relative path locally, pointing to a root that contains `artifacts/v<release-tag>/starkscan-cli-manifest.json` and matching platform archives. Example: `STARKSCAN_CLI_BUNDLED_ARTIFACT_DIR=/tmp/starkscan-cli-artifacts npx @starkscan/cli@
|
|
63
|
-
- `STARKSCAN_CLI_RELEASE_BASE_URL`: release base URL
|
|
129
|
+
- `STARKSCAN_CLI_BUNDLED_ARTIFACT_DIR`: maintainer/test override for the bundled artifact root. Use an absolute path in CI, or a cwd-relative path locally, pointing to a root that contains `artifacts/v<release-tag>/starkscan-cli-manifest.json` and matching platform archives. Example: `STARKSCAN_CLI_BUNDLED_ARTIFACT_DIR=/tmp/starkscan-cli-artifacts npx -y @starkscan/cli@0.1.0-beta.1 doctor`
|
|
130
|
+
- `STARKSCAN_CLI_RELEASE_BASE_URL`: maintainer/test release base URL override. Published public packages use bundled artifacts first; beta clients should not set this. By default the override must be a local `file:` URL, the official GitHub HTTPS release path, or Starkscan HTTPS. Custom HTTPS hosts or GitHub repo paths require `STARKSCAN_CLI_ALLOW_CUSTOM_RELEASE_BASE_URL=1`; plaintext HTTP requires `STARKSCAN_CLI_ALLOW_INSECURE_RELEASE_BASE_URL=1` and is limited to localhost or loopback maintainer fixtures. Network release manifests are fail-closed unless `STARKSCAN_CLI_ALLOW_UNSIGNED_NETWORK_MANIFEST=1` is set for a maintainer-only fallback; bundled npm artifacts or local `file:` fixtures are preferred until signed manifest verification exists.
|
|
64
131
|
- `STARKSCAN_CLI_CACHE_DIR`: cache root for downloaded native binaries
|
|
65
|
-
- `STARKSCAN_CLI_BIN_PATH`: use an existing local `starkscan` binary and skip download
|
|
66
|
-
- `STARKSCAN_CLI_RELEASE_REPO`: GitHub repo path
|
|
132
|
+
- `STARKSCAN_CLI_BIN_PATH`: maintainer/test override to use an existing local `starkscan` binary and skip download. Requires `STARKSCAN_CLI_ALLOW_BIN_PATH=1`; the path must be absolute, executable, and not a symlink.
|
|
133
|
+
- `STARKSCAN_CLI_RELEASE_REPO`: maintainer/test GitHub repo path override
|
|
67
134
|
- `STARKSCAN_CLI_PLATFORM_OVERRIDE`: force a specific target platform for cross-platform testing
|
|
68
135
|
- `STARKSCAN_CLI_DOWNLOAD_TIMEOUT_MS`: download inactivity timeout in milliseconds, default `120000`
|
|
69
136
|
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-
|
|
3
|
+
"generatedAt": "2026-06-29T04:53:36Z",
|
|
4
4
|
"artifacts": [
|
|
5
5
|
{
|
|
6
6
|
"name": "starkscan-darwin-aarch64.tar.gz",
|
|
7
7
|
"platform": "darwin-aarch64",
|
|
8
8
|
"target": "darwin-aarch64",
|
|
9
9
|
"archive": "starkscan-darwin-aarch64.tar.gz",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "b33a14b7b4e85ee571ee537e46153ea0ef5eb1eb01d6d34c3620c670c13cb371"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"name": "starkscan-darwin-x86_64.tar.gz",
|
|
14
14
|
"platform": "darwin-x86_64",
|
|
15
15
|
"target": "darwin-x86_64",
|
|
16
16
|
"archive": "starkscan-darwin-x86_64.tar.gz",
|
|
17
|
-
"sha256": "
|
|
17
|
+
"sha256": "4bcf906defb317754d38c72a9586219eadfcb9dc725aa920a4fce650c78cf311"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"name": "starkscan-linux-aarch64.tar.gz",
|
|
21
21
|
"platform": "linux-aarch64",
|
|
22
22
|
"target": "linux-aarch64",
|
|
23
23
|
"archive": "starkscan-linux-aarch64.tar.gz",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "a60a493fd6c226b329d9e7a4f1b1c1a0d62a68d59740dd484390306db879bac1"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "starkscan-linux-x86_64.tar.gz",
|
|
28
28
|
"platform": "linux-x86_64",
|
|
29
29
|
"target": "linux-x86_64",
|
|
30
30
|
"archive": "starkscan-linux-x86_64.tar.gz",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "604f25e483adf6b035e37ef803f6e5d5a56bed20b3caa1762d0a46923d384e05"
|
|
32
32
|
}
|
|
33
33
|
]
|
|
34
34
|
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
b33a14b7b4e85ee571ee537e46153ea0ef5eb1eb01d6d34c3620c670c13cb371 starkscan-darwin-aarch64.tar.gz
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4bcf906defb317754d38c72a9586219eadfcb9dc725aa920a4fce650c78cf311 starkscan-darwin-x86_64.tar.gz
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
a60a493fd6c226b329d9e7a4f1b1c1a0d62a68d59740dd484390306db879bac1 starkscan-linux-aarch64.tar.gz
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
604f25e483adf6b035e37ef803f6e5d5a56bed20b3caa1762d0a46923d384e05 starkscan-linux-x86_64.tar.gz
|
package/bin/starkscan.js
CHANGED
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { constants as osConstants } from 'node:os';
|
|
4
4
|
|
|
5
|
-
import { ensureStarkscanBinary } from '../scripts/installer.mjs';
|
|
5
|
+
import { ensureStarkscanBinary, resolveStarkscanBinary } from '../scripts/installer.mjs';
|
|
6
|
+
|
|
7
|
+
function truthyEnv(name) {
|
|
8
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
9
|
+
return value === '1' || value === 'true' || value === 'yes' || value === 'on';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shouldPrintLauncherJson(args) {
|
|
13
|
+
return args.includes('--launcher-json')
|
|
14
|
+
|| truthyEnv('STARKSCAN_LAUNCHER_JSON')
|
|
15
|
+
|| truthyEnv('STARKSCAN_CLI_LAUNCHER_JSON');
|
|
16
|
+
}
|
|
6
17
|
|
|
7
18
|
function exitFromResult(result) {
|
|
8
19
|
if (result.error) {
|
|
@@ -24,8 +35,15 @@ function exitFromResult(result) {
|
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
try {
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
if (shouldPrintLauncherJson(args)) {
|
|
40
|
+
const resolution = await resolveStarkscanBinary();
|
|
41
|
+
process.stdout.write(`${JSON.stringify(resolution, null, 2)}\n`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
const binaryPath = await ensureStarkscanBinary();
|
|
28
|
-
const result = spawnSync(binaryPath,
|
|
46
|
+
const result = spawnSync(binaryPath, args, {
|
|
29
47
|
stdio: 'inherit',
|
|
30
48
|
});
|
|
31
49
|
exitFromResult(result);
|
package/package.json
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starkscan/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.0-
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "0.1.0-beta.1",
|
|
5
|
+
"description": "Command-line launcher for Starkscan API, export, and agent workflows.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"publishConfig": {
|
|
9
|
-
"access": "public"
|
|
10
|
-
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://starkscan.co/docs/ai/agent-cli",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://starkscan.co/docs/build/package-trust"
|
|
11
14
|
},
|
|
12
15
|
"repository": {
|
|
13
16
|
"type": "git",
|
|
14
|
-
"url": "
|
|
17
|
+
"url": "https://github.com/starknet-innovation/mezcal",
|
|
15
18
|
"directory": "webapp/packages/cli"
|
|
16
19
|
},
|
|
17
|
-
"homepage": "https://github.com/starknet-innovation/mezcal/tree/main/webapp/packages/cli",
|
|
18
20
|
"keywords": [
|
|
19
21
|
"starkscan",
|
|
20
22
|
"starknet",
|
|
21
|
-
"explorer",
|
|
23
|
+
"block-explorer",
|
|
24
|
+
"cairo",
|
|
25
|
+
"web3",
|
|
22
26
|
"cli",
|
|
23
|
-
"agent"
|
|
27
|
+
"agent",
|
|
28
|
+
"npx"
|
|
24
29
|
],
|
|
25
30
|
"files": [
|
|
26
31
|
"artifacts",
|
|
@@ -38,9 +43,11 @@
|
|
|
38
43
|
"prepack": "npm run verify:bundled && npm run typecheck && npm test",
|
|
39
44
|
"prepublishOnly": "npm run verify:bundled && npm run typecheck && npm test",
|
|
40
45
|
"publish:alpha:dry-run": "./scripts/publish-public-cli.sh --tag alpha --dry-run",
|
|
41
|
-
"test": "node ./scripts/tests/cli-wrapper.mjs && ./scripts/tests/publish-public-cli.sh",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
46
|
+
"test": "node ./scripts/tests/cli-wrapper.mjs && ./scripts/tests/publish-public-cli.sh && ./scripts/tests/e2e-canary-channel.sh",
|
|
47
|
+
"test:e2e": "node ./scripts/tests/e2e-canary.mjs",
|
|
48
|
+
"typecheck": "node --check ./bin/starkscan.js && node --check ./scripts/install.mjs && node --check ./scripts/installer.mjs && node --check ./scripts/verify-bundled-artifacts.mjs && node --check ./scripts/tests/cli-wrapper.mjs && node --check ./scripts/tests/e2e-canary.mjs",
|
|
49
|
+
"verify:bundled": "node ./scripts/verify-bundled-artifacts.mjs",
|
|
50
|
+
"publish:beta:dry-run": "./scripts/publish-public-cli.sh --tag beta --dry-run"
|
|
44
51
|
},
|
|
45
52
|
"engines": {
|
|
46
53
|
"node": ">=18"
|
package/scripts/installer.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
3
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
4
|
-
import { copyFile, chmod, lstat, mkdir, mkdtemp, readFile, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import { copyFile, chmod, lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { get as httpGet } from 'node:http';
|
|
6
6
|
import { get as httpsGet } from 'node:https';
|
|
7
|
+
import { isIP } from 'node:net';
|
|
7
8
|
import os from 'node:os';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -11,10 +12,15 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
11
12
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
13
|
const PACKAGE_JSON_PATH = path.join(PACKAGE_ROOT, 'package.json');
|
|
13
14
|
const DEFAULT_BUNDLED_ARTIFACT_ROOT = path.join(PACKAGE_ROOT, 'artifacts');
|
|
14
|
-
const DEFAULT_REPO = 'starknet-innovation/
|
|
15
|
+
const DEFAULT_REPO = 'starknet-innovation/mezcal';
|
|
15
16
|
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
16
17
|
const MAX_REDIRECTS = 10;
|
|
17
18
|
const RELEASE_TAG_RE = /^(?=.*[A-Za-z0-9])(?!-)(?!.*\.\.)[A-Za-z0-9._-]+$/;
|
|
19
|
+
const ALLOWED_RELEASE_BASE_HOSTS = new Set(['github.com', 'starkscan.co']);
|
|
20
|
+
const ALLOWED_RELEASE_REDIRECT_HOSTS = new Set([
|
|
21
|
+
'objects.githubusercontent.com',
|
|
22
|
+
'release-assets.githubusercontent.com',
|
|
23
|
+
]);
|
|
18
24
|
const SUPPORTED_PLATFORMS = new Set([
|
|
19
25
|
'darwin-aarch64',
|
|
20
26
|
'darwin-x86_64',
|
|
@@ -22,9 +28,11 @@ const SUPPORTED_PLATFORMS = new Set([
|
|
|
22
28
|
'linux-x86_64',
|
|
23
29
|
]);
|
|
24
30
|
const PUBLIC_BINARY_NAME = 'starkscan';
|
|
25
|
-
const LEGACY_BINARY_NAME = 'mezcal';
|
|
26
31
|
const PUBLIC_MANIFEST_FILENAME = 'starkscan-cli-manifest.json';
|
|
27
|
-
const
|
|
32
|
+
const EXECUTABLE_CHECKS = ['not-symlink', 'regular-file', 'executable'];
|
|
33
|
+
const LOCAL_BIN_PATH_CHECKS = ['absolute-path', ...EXECUTABLE_CHECKS];
|
|
34
|
+
const KNOWN_CACHE_SOURCES = new Set(['bundled', 'download']);
|
|
35
|
+
const MANIFEST_TRUST_CHECKS = new Set(['local-release-manifest', 'unsigned-network-manifest-opt-in']);
|
|
28
36
|
|
|
29
37
|
function env(name) {
|
|
30
38
|
const value = process.env[name]?.trim();
|
|
@@ -39,6 +47,11 @@ function cliEnv(suffix) {
|
|
|
39
47
|
return envFirst(`STARKSCAN_CLI_${suffix}`, `MEZCAL_CLI_${suffix}`);
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
function cliFlag(suffix) {
|
|
51
|
+
const value = cliEnv(suffix)?.toLowerCase();
|
|
52
|
+
return value === '1' || value === 'true' || value === 'yes' || value === 'on';
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
function fail(message) {
|
|
43
56
|
throw new Error(message);
|
|
44
57
|
}
|
|
@@ -171,8 +184,9 @@ function releaseTagFromVersion(version) {
|
|
|
171
184
|
return version.startsWith('v') ? version : `v${version}`;
|
|
172
185
|
}
|
|
173
186
|
|
|
174
|
-
async function resolveReleaseTag() {
|
|
175
|
-
const
|
|
187
|
+
async function resolveReleaseTag(version) {
|
|
188
|
+
const packageVersionForTag = version ?? await packageVersion();
|
|
189
|
+
const tag = cliEnv('RELEASE_TAG') ?? env('MEZCAL_INSTALL_VERSION') ?? releaseTagFromVersion(packageVersionForTag);
|
|
176
190
|
if (tag !== 'latest' && !RELEASE_TAG_RE.test(tag)) {
|
|
177
191
|
fail(`unsupported release tag: ${tag}`);
|
|
178
192
|
}
|
|
@@ -180,17 +194,137 @@ async function resolveReleaseTag() {
|
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
function releaseRootUrl(baseUrl, releaseTag) {
|
|
183
|
-
const normalizedBase = baseUrl.replace(/\/+$/, '');
|
|
197
|
+
const normalizedBase = normalizeReleaseBaseUrl(baseUrl).replace(/\/+$/, '');
|
|
184
198
|
if (releaseTag === 'latest') {
|
|
185
199
|
return `${normalizedBase}/latest/download`;
|
|
186
200
|
}
|
|
187
201
|
return `${normalizedBase}/download/${releaseTag}`;
|
|
188
202
|
}
|
|
189
203
|
|
|
204
|
+
function releaseBaseHostIsAllowed(hostname) {
|
|
205
|
+
return ALLOWED_RELEASE_BASE_HOSTS.has(hostname) || hostname.endsWith('.starkscan.co');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function releaseRedirectHttpsHostIsAllowed(hostname) {
|
|
209
|
+
return releaseBaseHostIsAllowed(hostname) || ALLOWED_RELEASE_REDIRECT_HOSTS.has(hostname);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function releaseBaseHttpHostIsLocal(hostname) {
|
|
213
|
+
const normalized = hostname.toLowerCase();
|
|
214
|
+
if (normalized === 'localhost') {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
const address = normalized.startsWith('[') && normalized.endsWith(']')
|
|
218
|
+
? normalized.slice(1, -1)
|
|
219
|
+
: normalized;
|
|
220
|
+
if (address === '::1') {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (isIP(address) !== 4) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const octets = address.split('.').map(Number);
|
|
227
|
+
return octets.length === 4 && octets[0] === 127 && octets.every((octet) => octet >= 0 && octet <= 255);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function releaseGitHubPathMatchesDefaultRepo(pathname) {
|
|
231
|
+
const normalized = pathname.replace(/\/+$/, '');
|
|
232
|
+
const [owner, repo, segment, ...extra] = normalized.split('/').filter(Boolean);
|
|
233
|
+
const [expectedOwner, expectedRepo] = DEFAULT_REPO.split('/');
|
|
234
|
+
const matchesRepo =
|
|
235
|
+
owner?.toLowerCase() === expectedOwner.toLowerCase()
|
|
236
|
+
&& repo?.toLowerCase() === expectedRepo.toLowerCase()
|
|
237
|
+
&& segment === 'releases';
|
|
238
|
+
return { matchesRepo, extra };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function releaseBaseGitHubPathIsAllowed(pathname) {
|
|
242
|
+
const { matchesRepo, extra } = releaseGitHubPathMatchesDefaultRepo(pathname);
|
|
243
|
+
return matchesRepo && extra.length === 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function releaseRedirectGitHubPathIsAllowed(pathname) {
|
|
247
|
+
const { matchesRepo } = releaseGitHubPathMatchesDefaultRepo(pathname);
|
|
248
|
+
return matchesRepo;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function normalizeReleaseBaseUrl(baseUrl) {
|
|
252
|
+
let parsed;
|
|
253
|
+
try {
|
|
254
|
+
parsed = new URL(baseUrl);
|
|
255
|
+
} catch {
|
|
256
|
+
fail(`unsupported STARKSCAN_CLI_RELEASE_BASE_URL: ${baseUrl}`);
|
|
257
|
+
}
|
|
258
|
+
if (parsed.search || parsed.hash) {
|
|
259
|
+
fail('STARKSCAN_CLI_RELEASE_BASE_URL must not include query parameters or fragments');
|
|
260
|
+
}
|
|
261
|
+
if (parsed.username || parsed.password) {
|
|
262
|
+
fail('STARKSCAN_CLI_RELEASE_BASE_URL must not include credentials');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (parsed.protocol === 'file:') {
|
|
266
|
+
try {
|
|
267
|
+
const filePath = fileURLToPath(parsed);
|
|
268
|
+
if (!path.isAbsolute(filePath)) {
|
|
269
|
+
fail('STARKSCAN_CLI_RELEASE_BASE_URL file: override must resolve to an absolute path');
|
|
270
|
+
}
|
|
271
|
+
return pathToFileURL(filePath).href;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error instanceof Error && error.message.includes('STARKSCAN_CLI_RELEASE_BASE_URL')) {
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
fail('STARKSCAN_CLI_RELEASE_BASE_URL file: override must be an absolute local file URL');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (parsed.protocol === 'http:') {
|
|
281
|
+
if (!cliFlag('ALLOW_INSECURE_RELEASE_BASE_URL')) {
|
|
282
|
+
fail('STARKSCAN_CLI_RELEASE_BASE_URL must use HTTPS or file: unless STARKSCAN_CLI_ALLOW_INSECURE_RELEASE_BASE_URL=1 is set for a local maintainer test');
|
|
283
|
+
}
|
|
284
|
+
if (!releaseBaseHttpHostIsLocal(parsed.hostname)) {
|
|
285
|
+
fail('plaintext HTTP STARKSCAN_CLI_RELEASE_BASE_URL is only allowed for localhost or loopback maintainer fixtures');
|
|
286
|
+
}
|
|
287
|
+
return parsed.href;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (parsed.protocol !== 'https:') {
|
|
291
|
+
fail(`unsupported STARKSCAN_CLI_RELEASE_BASE_URL protocol: ${parsed.protocol}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!releaseBaseHostIsAllowed(parsed.hostname) && !cliFlag('ALLOW_CUSTOM_RELEASE_BASE_URL')) {
|
|
295
|
+
fail(`custom STARKSCAN_CLI_RELEASE_BASE_URL host is not allowed by default: ${parsed.hostname}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (parsed.hostname === 'github.com' && !cliFlag('ALLOW_CUSTOM_RELEASE_BASE_URL') && !releaseBaseGitHubPathIsAllowed(parsed.pathname)) {
|
|
299
|
+
fail(`custom STARKSCAN_CLI_RELEASE_BASE_URL GitHub repository is not allowed by default: ${parsed.pathname}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return parsed.href;
|
|
303
|
+
}
|
|
304
|
+
|
|
190
305
|
function releaseAssetUrl(rootUrl, filename) {
|
|
191
306
|
return `${rootUrl}/${filename}`;
|
|
192
307
|
}
|
|
193
308
|
|
|
309
|
+
function releaseRootUsesNetwork(rootUrl) {
|
|
310
|
+
const protocol = new URL(rootUrl).protocol;
|
|
311
|
+
return protocol === 'https:' || protocol === 'http:';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function releaseManifestTrustCheck(rootUrl) {
|
|
315
|
+
if (!releaseRootUsesNetwork(rootUrl)) {
|
|
316
|
+
return 'local-release-manifest';
|
|
317
|
+
}
|
|
318
|
+
if (!cliFlag('ALLOW_UNSIGNED_NETWORK_MANIFEST')) {
|
|
319
|
+
fail(
|
|
320
|
+
'unsigned network release manifests are not trusted by default; use bundled npm artifacts, '
|
|
321
|
+
+ 'a local file: release source, or set STARKSCAN_CLI_ALLOW_UNSIGNED_NETWORK_MANIFEST=1 '
|
|
322
|
+
+ 'for a maintainer-only fallback until signed manifest verification is available',
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
return 'unsigned-network-manifest-opt-in';
|
|
326
|
+
}
|
|
327
|
+
|
|
194
328
|
function bundledArtifactRoot() {
|
|
195
329
|
const configured = cliEnv('BUNDLED_ARTIFACT_DIR');
|
|
196
330
|
if (!configured) {
|
|
@@ -204,14 +338,130 @@ function bundledArtifactRootUrl(releaseTag) {
|
|
|
204
338
|
}
|
|
205
339
|
|
|
206
340
|
async function hasBundledArtifacts(releaseTag) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
341
|
+
const manifestPath = path.join(bundledArtifactRoot(), releaseTag, PUBLIC_MANIFEST_FILENAME);
|
|
342
|
+
const stat = await lstat(manifestPath).catch(() => null);
|
|
343
|
+
return Boolean(stat?.isFile());
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function resolveReleaseSource(releaseTag) {
|
|
347
|
+
const releaseRepo = cliEnv('RELEASE_REPO');
|
|
348
|
+
const repo = releaseRepo ?? DEFAULT_REPO;
|
|
349
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
|
|
350
|
+
fail(`unsupported GitHub repo path: ${repo}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const releaseBaseUrl = cliEnv('RELEASE_BASE_URL');
|
|
354
|
+
if (
|
|
355
|
+
!releaseBaseUrl &&
|
|
356
|
+
!releaseRepo &&
|
|
357
|
+
await hasBundledArtifacts(releaseTag)
|
|
358
|
+
) {
|
|
359
|
+
return {
|
|
360
|
+
rootUrl: bundledArtifactRootUrl(releaseTag),
|
|
361
|
+
source: 'bundled',
|
|
362
|
+
requestedFrom: 'bundled-artifacts',
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const baseUrl = releaseBaseUrl ?? `https://github.com/${repo}/releases`;
|
|
367
|
+
return {
|
|
368
|
+
rootUrl: releaseRootUrl(baseUrl, releaseTag),
|
|
369
|
+
source: 'download',
|
|
370
|
+
requestedFrom: releaseBaseUrl ? 'release-base-url' : 'github-release',
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function cacheMetadataPath(cachePath) {
|
|
375
|
+
return `${cachePath}.meta.json`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function writeCacheMetadata(cachePath, metadata) {
|
|
379
|
+
const metadataPath = cacheMetadataPath(cachePath);
|
|
380
|
+
const stagePath = `${metadataPath}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
381
|
+
try {
|
|
382
|
+
await writeFile(stagePath, `${JSON.stringify({ schemaVersion: 1, ...metadata }, null, 2)}\n`);
|
|
383
|
+
await rename(stagePath, metadataPath);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
await rm(stagePath, { force: true }).catch(() => {});
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function readCacheMetadata(cachePath, releaseTag, platform) {
|
|
391
|
+
const metadataPath = cacheMetadataPath(cachePath);
|
|
392
|
+
try {
|
|
393
|
+
const parsed = JSON.parse(await readFile(metadataPath, 'utf8'));
|
|
394
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
395
|
+
throw new Error('cache metadata root must be an object');
|
|
396
|
+
}
|
|
397
|
+
if (parsed.schemaVersion !== 1) {
|
|
398
|
+
throw new Error('cache metadata schemaVersion must be 1');
|
|
399
|
+
}
|
|
400
|
+
if (parsed.releaseTag !== releaseTag || parsed.platform !== platform) {
|
|
401
|
+
throw new Error('cache metadata does not match cache path');
|
|
402
|
+
}
|
|
403
|
+
if (typeof parsed.source !== 'string' || !KNOWN_CACHE_SOURCES.has(parsed.source)) {
|
|
404
|
+
throw new Error('cache metadata source is invalid');
|
|
212
405
|
}
|
|
406
|
+
const storedChecks = Array.isArray(parsed.verification?.checks)
|
|
407
|
+
? parsed.verification.checks.filter((check) => typeof check === 'string')
|
|
408
|
+
: [];
|
|
409
|
+
const manifestTrustChecks = storedChecks.filter((check) => MANIFEST_TRUST_CHECKS.has(check));
|
|
410
|
+
return {
|
|
411
|
+
source: parsed.source,
|
|
412
|
+
requestedFrom: typeof parsed.requestedFrom === 'string' ? parsed.requestedFrom : 'cache-metadata',
|
|
413
|
+
releaseRootUrl: typeof parsed.releaseRootUrl === 'string' ? parsed.releaseRootUrl : null,
|
|
414
|
+
manifestTrustChecks,
|
|
415
|
+
sourceConfidence: 'stored',
|
|
416
|
+
metadataStatus: 'stored',
|
|
417
|
+
};
|
|
418
|
+
} catch (error) {
|
|
419
|
+
const errorCode = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
|
|
420
|
+
const isMissing = errorCode === 'ENOENT';
|
|
421
|
+
return {
|
|
422
|
+
source: 'unknown',
|
|
423
|
+
requestedFrom: isMissing ? 'legacy-cache-missing-metadata' : 'cache-metadata-invalid',
|
|
424
|
+
releaseRootUrl: null,
|
|
425
|
+
manifestTrustChecks: [],
|
|
426
|
+
sourceConfidence: 'unknown',
|
|
427
|
+
metadataStatus: isMissing ? 'missing' : 'invalid',
|
|
428
|
+
metadataError: isMissing ? undefined : (error instanceof SyntaxError ? 'invalid-json' : errorCode ?? 'invalid-metadata'),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function enforceCachedManifestTrustChecks(cachedSource) {
|
|
434
|
+
const manifestTrustChecks = cachedSource.manifestTrustChecks ?? [];
|
|
435
|
+
if (cachedSource.metadataStatus !== 'stored') {
|
|
436
|
+
fail(
|
|
437
|
+
'cached Starkscan binary is missing trusted cache metadata; delete the cache and reinstall '
|
|
438
|
+
+ 'from bundled npm artifacts or a local file: release source',
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (manifestTrustChecks.length === 0) {
|
|
442
|
+
fail(
|
|
443
|
+
'cached Starkscan binary is missing release manifest trust provenance; delete the cache and reinstall '
|
|
444
|
+
+ 'from bundled npm artifacts or a local file: release source',
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
if (
|
|
448
|
+
manifestTrustChecks.includes('unsigned-network-manifest-opt-in') &&
|
|
449
|
+
!cliFlag('ALLOW_UNSIGNED_NETWORK_MANIFEST')
|
|
450
|
+
) {
|
|
451
|
+
fail(
|
|
452
|
+
'cached Starkscan binary was installed from an unsigned network release manifest; '
|
|
453
|
+
+ 'set STARKSCAN_CLI_ALLOW_UNSIGNED_NETWORK_MANIFEST=1 to reuse this maintainer-only '
|
|
454
|
+
+ 'fallback cache, or delete the cache and reinstall from bundled npm artifacts or a local file: release source',
|
|
455
|
+
);
|
|
213
456
|
}
|
|
214
|
-
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function launcherResolution(fields) {
|
|
460
|
+
return {
|
|
461
|
+
schemaVersion: 1,
|
|
462
|
+
packageName: '@starkscan/cli',
|
|
463
|
+
...fields,
|
|
464
|
+
};
|
|
215
465
|
}
|
|
216
466
|
|
|
217
467
|
export function resolveRedirectUrl(url, location, redirectsLeft) {
|
|
@@ -226,6 +476,27 @@ export function resolveRedirectUrl(url, location, redirectsLeft) {
|
|
|
226
476
|
if (currentUrl.protocol === 'https:' && redirectUrl.protocol === 'http:') {
|
|
227
477
|
fail(`redirect from HTTPS to HTTP is not allowed: ${redirectUrl.toString()}`);
|
|
228
478
|
}
|
|
479
|
+
if (redirectUrl.username || redirectUrl.password) {
|
|
480
|
+
fail('release asset redirect URL must not include credentials');
|
|
481
|
+
}
|
|
482
|
+
if (redirectUrl.protocol === 'http:' && !releaseBaseHttpHostIsLocal(redirectUrl.hostname)) {
|
|
483
|
+
fail('plaintext HTTP release asset redirect is only allowed for localhost or loopback maintainer fixtures');
|
|
484
|
+
}
|
|
485
|
+
if (
|
|
486
|
+
redirectUrl.protocol === 'https:' &&
|
|
487
|
+
!releaseRedirectHttpsHostIsAllowed(redirectUrl.hostname) &&
|
|
488
|
+
!cliFlag('ALLOW_CUSTOM_RELEASE_BASE_URL')
|
|
489
|
+
) {
|
|
490
|
+
fail(`custom release asset redirect host is not allowed by default: ${redirectUrl.hostname}`);
|
|
491
|
+
}
|
|
492
|
+
if (
|
|
493
|
+
redirectUrl.protocol === 'https:' &&
|
|
494
|
+
redirectUrl.hostname === 'github.com' &&
|
|
495
|
+
!cliFlag('ALLOW_CUSTOM_RELEASE_BASE_URL') &&
|
|
496
|
+
!releaseRedirectGitHubPathIsAllowed(redirectUrl.pathname)
|
|
497
|
+
) {
|
|
498
|
+
fail(`custom release asset redirect GitHub repository is not allowed by default: ${redirectUrl.pathname}`);
|
|
499
|
+
}
|
|
229
500
|
return redirectUrl.toString();
|
|
230
501
|
}
|
|
231
502
|
|
|
@@ -322,15 +593,15 @@ function validateArtifact(artifact, platform) {
|
|
|
322
593
|
if (artifact.platform !== platform) {
|
|
323
594
|
fail(`release manifest selected wrong platform: ${artifact.platform}`);
|
|
324
595
|
}
|
|
325
|
-
const
|
|
596
|
+
const expectedArchive = `starkscan-${platform}.tar.gz`;
|
|
326
597
|
if (artifact.archive === undefined && artifact.name === undefined) {
|
|
327
|
-
fail(`release manifest artifact for ${platform} must use
|
|
598
|
+
fail(`release manifest artifact for ${platform} must use ${expectedArchive}`);
|
|
328
599
|
}
|
|
329
|
-
if (artifact.archive !== undefined &&
|
|
330
|
-
fail(`release manifest artifact archive for ${platform} must be
|
|
600
|
+
if (artifact.archive !== undefined && artifact.archive !== expectedArchive) {
|
|
601
|
+
fail(`release manifest artifact archive for ${platform} must be ${expectedArchive}`);
|
|
331
602
|
}
|
|
332
|
-
if (artifact.name !== undefined &&
|
|
333
|
-
fail(`release manifest artifact name for ${platform} must be
|
|
603
|
+
if (artifact.name !== undefined && artifact.name !== expectedArchive) {
|
|
604
|
+
fail(`release manifest artifact name for ${platform} must be ${expectedArchive}`);
|
|
334
605
|
}
|
|
335
606
|
if (artifact.archive !== undefined && artifact.name !== undefined && artifact.archive !== artifact.name) {
|
|
336
607
|
fail(`release manifest artifact name and archive for ${platform} must match`);
|
|
@@ -345,27 +616,23 @@ function validateArtifact(artifact, platform) {
|
|
|
345
616
|
}
|
|
346
617
|
|
|
347
618
|
async function readManifest(rootUrl, tempDir) {
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (!isMissingManifestDownload(error)) {
|
|
356
|
-
fail(`release manifest ${manifestFilename} could not be downloaded: ${errorMessage(error)}`);
|
|
357
|
-
}
|
|
358
|
-
attempts.push(`${manifestFilename}: ${errorMessage(error)}`);
|
|
359
|
-
continue;
|
|
619
|
+
const manifestPath = path.join(tempDir, PUBLIC_MANIFEST_FILENAME);
|
|
620
|
+
try {
|
|
621
|
+
await downloadFile(releaseAssetUrl(rootUrl, PUBLIC_MANIFEST_FILENAME), manifestPath);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
await rm(manifestPath, { force: true }).catch(() => {});
|
|
624
|
+
if (!isMissingManifestDownload(error)) {
|
|
625
|
+
fail(`release manifest ${PUBLIC_MANIFEST_FILENAME} could not be downloaded: ${errorMessage(error)}`);
|
|
360
626
|
}
|
|
361
|
-
|
|
627
|
+
fail(`release manifest ${PUBLIC_MANIFEST_FILENAME} not found: ${errorMessage(error)}`);
|
|
362
628
|
}
|
|
363
|
-
|
|
629
|
+
return await readDownloadedManifest(manifestPath, PUBLIC_MANIFEST_FILENAME);
|
|
364
630
|
}
|
|
365
631
|
|
|
366
|
-
async function installFromRelease({ platform, releaseTag, cachePath, rootUrl }) {
|
|
632
|
+
async function installFromRelease({ platform, releaseTag, cachePath, rootUrl, source, requestedFrom }) {
|
|
367
633
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'starkscan-npm-cli-'));
|
|
368
634
|
try {
|
|
635
|
+
const manifestTrustCheck = releaseManifestTrustCheck(rootUrl);
|
|
369
636
|
const manifest = await readManifest(rootUrl, tempDir);
|
|
370
637
|
const matching = manifest.artifacts.filter((artifact) => artifact?.platform === platform);
|
|
371
638
|
if (matching.length !== 1) {
|
|
@@ -382,7 +649,7 @@ async function installFromRelease({ platform, releaseTag, cachePath, rootUrl })
|
|
|
382
649
|
}
|
|
383
650
|
|
|
384
651
|
const listing = runChecked('tar', ['-tzf', archivePath]).stdout.trim();
|
|
385
|
-
if (listing !== PUBLIC_BINARY_NAME
|
|
652
|
+
if (listing !== PUBLIC_BINARY_NAME) {
|
|
386
653
|
fail(`release artifact ${artifact.archive} must contain exactly one top-level Starkscan binary`);
|
|
387
654
|
}
|
|
388
655
|
|
|
@@ -403,21 +670,75 @@ async function installFromRelease({ platform, releaseTag, cachePath, rootUrl })
|
|
|
403
670
|
throw error;
|
|
404
671
|
}
|
|
405
672
|
await validateExecutableFile(cachePath, 'cached Starkscan binary');
|
|
406
|
-
|
|
673
|
+
const result = {
|
|
674
|
+
binaryPath: cachePath,
|
|
675
|
+
platform,
|
|
676
|
+
releaseTag,
|
|
677
|
+
source,
|
|
678
|
+
requestedFrom,
|
|
679
|
+
resolvedFrom: 'release',
|
|
680
|
+
cacheHit: false,
|
|
681
|
+
cachePath,
|
|
682
|
+
releaseRootUrl: rootUrl,
|
|
683
|
+
verification: {
|
|
684
|
+
status: 'verified-sha256',
|
|
685
|
+
manifest: PUBLIC_MANIFEST_FILENAME,
|
|
686
|
+
archive: artifact.archive,
|
|
687
|
+
sha256: artifact.sha256,
|
|
688
|
+
checks: [manifestTrustCheck, 'manifest-schema', 'platform-match', 'archive-name', 'archive-sha256', 'single-binary-archive', ...EXECUTABLE_CHECKS],
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
await writeCacheMetadata(cachePath, {
|
|
692
|
+
platform,
|
|
693
|
+
releaseTag,
|
|
694
|
+
source,
|
|
695
|
+
requestedFrom,
|
|
696
|
+
releaseRootUrl: rootUrl,
|
|
697
|
+
verification: {
|
|
698
|
+
status: result.verification.status,
|
|
699
|
+
manifest: result.verification.manifest,
|
|
700
|
+
archive: result.verification.archive,
|
|
701
|
+
sha256: result.verification.sha256,
|
|
702
|
+
checks: result.verification.checks,
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
return result;
|
|
407
706
|
} finally {
|
|
408
707
|
await rm(tempDir, { recursive: true, force: true });
|
|
409
708
|
}
|
|
410
709
|
}
|
|
411
710
|
|
|
412
|
-
export async function
|
|
711
|
+
export async function resolveStarkscanBinary(options = {}) {
|
|
712
|
+
const version = await packageVersion();
|
|
413
713
|
const explicitBinary = cliEnv('BIN_PATH');
|
|
414
714
|
if (explicitBinary) {
|
|
715
|
+
if (!cliFlag('ALLOW_BIN_PATH')) {
|
|
716
|
+
fail('STARKSCAN_CLI_BIN_PATH requires STARKSCAN_CLI_ALLOW_BIN_PATH=1');
|
|
717
|
+
}
|
|
718
|
+
if (!path.isAbsolute(explicitBinary)) {
|
|
719
|
+
fail(`STARKSCAN_CLI_BIN_PATH must be an absolute path: ${explicitBinary}`);
|
|
720
|
+
}
|
|
415
721
|
await validateExecutableFile(explicitBinary, 'STARKSCAN_CLI_BIN_PATH');
|
|
416
|
-
return
|
|
722
|
+
return launcherResolution({
|
|
723
|
+
packageVersion: version,
|
|
724
|
+
binaryPath: explicitBinary,
|
|
725
|
+
platform: null,
|
|
726
|
+
releaseTag: null,
|
|
727
|
+
source: 'bin-path',
|
|
728
|
+
requestedFrom: 'bin-path',
|
|
729
|
+
resolvedFrom: 'bin-path',
|
|
730
|
+
cacheHit: false,
|
|
731
|
+
cachePath: null,
|
|
732
|
+
releaseRootUrl: null,
|
|
733
|
+
verification: {
|
|
734
|
+
status: 'validated-local-executable',
|
|
735
|
+
checks: LOCAL_BIN_PATH_CHECKS,
|
|
736
|
+
},
|
|
737
|
+
});
|
|
417
738
|
}
|
|
418
739
|
|
|
419
740
|
const platform = resolvePlatform();
|
|
420
|
-
const releaseTag = await resolveReleaseTag();
|
|
741
|
+
const releaseTag = await resolveReleaseTag(version);
|
|
421
742
|
const cacheDir = path.join(defaultCacheRoot(), releaseTag, platform);
|
|
422
743
|
const cachePath = path.join(cacheDir, PUBLIC_BINARY_NAME);
|
|
423
744
|
const allowCacheReuse = !options.force && releaseTag !== 'latest';
|
|
@@ -425,47 +746,45 @@ export async function ensureStarkscanBinary(options = {}) {
|
|
|
425
746
|
const existing = await lstat(cachePath).catch(() => null);
|
|
426
747
|
if (existing) {
|
|
427
748
|
await validateExecutableFile(cachePath, 'cached Starkscan binary');
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
749
|
+
const cachedSource = await readCacheMetadata(cachePath, releaseTag, platform);
|
|
750
|
+
enforceCachedManifestTrustChecks(cachedSource);
|
|
751
|
+
return launcherResolution({
|
|
752
|
+
packageVersion: version,
|
|
753
|
+
binaryPath: cachePath,
|
|
754
|
+
platform,
|
|
755
|
+
releaseTag,
|
|
756
|
+
source: cachedSource.source,
|
|
757
|
+
requestedFrom: cachedSource.requestedFrom,
|
|
758
|
+
resolvedFrom: 'cache',
|
|
759
|
+
cacheHit: true,
|
|
760
|
+
cachePath,
|
|
761
|
+
releaseRootUrl: cachedSource.releaseRootUrl,
|
|
762
|
+
sourceConfidence: cachedSource.sourceConfidence,
|
|
763
|
+
metadataStatus: cachedSource.metadataStatus,
|
|
764
|
+
...(cachedSource.metadataError ? { metadataError: cachedSource.metadataError } : {}),
|
|
765
|
+
verification: {
|
|
766
|
+
status: 'validated-cached-executable',
|
|
767
|
+
checks: [...(cachedSource.manifestTrustChecks ?? []), ...EXECUTABLE_CHECKS],
|
|
768
|
+
},
|
|
769
|
+
});
|
|
446
770
|
}
|
|
447
771
|
}
|
|
448
772
|
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
)
|
|
461
|
-
rootUrl = bundledArtifactRootUrl(releaseTag);
|
|
462
|
-
} else {
|
|
463
|
-
const baseUrl = releaseBaseUrl ?? `https://github.com/${repo}/releases`;
|
|
464
|
-
rootUrl = releaseRootUrl(baseUrl, releaseTag);
|
|
465
|
-
}
|
|
466
|
-
return installFromRelease({ platform, releaseTag, cachePath, rootUrl });
|
|
773
|
+
const releaseSource = await resolveReleaseSource(releaseTag);
|
|
774
|
+
return launcherResolution({
|
|
775
|
+
packageVersion: version,
|
|
776
|
+
...await installFromRelease({
|
|
777
|
+
platform,
|
|
778
|
+
releaseTag,
|
|
779
|
+
cachePath,
|
|
780
|
+
rootUrl: releaseSource.rootUrl,
|
|
781
|
+
source: releaseSource.source,
|
|
782
|
+
requestedFrom: releaseSource.requestedFrom,
|
|
783
|
+
}),
|
|
784
|
+
});
|
|
467
785
|
}
|
|
468
786
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
787
|
+
export async function ensureStarkscanBinary(options = {}) {
|
|
788
|
+
const resolution = await resolveStarkscanBinary(options);
|
|
789
|
+
return resolution.binaryPath;
|
|
790
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
import { createReadStream } from 'node:fs';
|
|
3
|
-
import { lstat, readFile } from 'node:fs/promises';
|
|
4
|
+
import { lstat, mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
4
6
|
import path from 'node:path';
|
|
5
7
|
import { fileURLToPath } from 'node:url';
|
|
6
8
|
|
|
@@ -15,7 +17,14 @@ const SUPPORTED_PLATFORMS = [
|
|
|
15
17
|
'linux-x86_64',
|
|
16
18
|
];
|
|
17
19
|
const PUBLIC_MANIFEST_FILENAME = 'starkscan-cli-manifest.json';
|
|
18
|
-
const
|
|
20
|
+
const PUBLIC_BINARY_NAME = 'starkscan';
|
|
21
|
+
const HOSTED_DEFAULT_BASE_URL = 'https://api.starkscan.co';
|
|
22
|
+
const PUBLIC_HELP_FORBIDDEN_STRINGS = [
|
|
23
|
+
'http://127.0.0.1:3000',
|
|
24
|
+
'--internal-api-key',
|
|
25
|
+
'STARKSCAN_INTERNAL_API_KEY',
|
|
26
|
+
'X-Explorer-Internal-Key',
|
|
27
|
+
];
|
|
19
28
|
|
|
20
29
|
function env(name) {
|
|
21
30
|
const value = process.env[name]?.trim();
|
|
@@ -30,6 +39,26 @@ function fail(message) {
|
|
|
30
39
|
throw new Error(`verify-bundled-artifacts: ${message}`);
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
function describeSpawn(result) {
|
|
43
|
+
const details = [
|
|
44
|
+
`status=${result.status ?? 'null'}`,
|
|
45
|
+
`signal=${result.signal ?? 'none'}`,
|
|
46
|
+
];
|
|
47
|
+
if (result.error?.message) {
|
|
48
|
+
details.push(`error=${result.error.message}`);
|
|
49
|
+
}
|
|
50
|
+
for (const [name, value] of [
|
|
51
|
+
['stdout', result.stdout],
|
|
52
|
+
['stderr', result.stderr],
|
|
53
|
+
]) {
|
|
54
|
+
const text = String(value ?? '').trim();
|
|
55
|
+
if (text.length > 0) {
|
|
56
|
+
details.push(`${name}=${text}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return details.join('; ');
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
function releaseTagFromVersion(version) {
|
|
34
63
|
return version.startsWith('v') ? version : `v${version}`;
|
|
35
64
|
}
|
|
@@ -51,6 +80,97 @@ async function assertFile(filePath, label, remediation) {
|
|
|
51
80
|
}
|
|
52
81
|
}
|
|
53
82
|
|
|
83
|
+
function currentReleasePlatform() {
|
|
84
|
+
const os =
|
|
85
|
+
process.platform === 'darwin' || process.platform === 'linux'
|
|
86
|
+
? process.platform
|
|
87
|
+
: undefined;
|
|
88
|
+
const arch =
|
|
89
|
+
process.arch === 'x64' ? 'x86_64' : process.arch === 'arm64' ? 'aarch64' : undefined;
|
|
90
|
+
return os && arch ? `${os}-${arch}` : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function publicVerifierSpawnEnv() {
|
|
94
|
+
const nextEnv = {};
|
|
95
|
+
for (const name of ['PATH', 'TMPDIR', 'TMP', 'TEMP', 'LANG', 'LC_ALL', 'LC_CTYPE']) {
|
|
96
|
+
const value = process.env[name];
|
|
97
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
98
|
+
nextEnv[name] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!nextEnv.PATH) {
|
|
102
|
+
nextEnv.PATH = '/usr/bin:/bin:/usr/sbin:/sbin';
|
|
103
|
+
}
|
|
104
|
+
return nextEnv;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function verifyCurrentPlatformHelpContract(releaseRoot, manifest) {
|
|
108
|
+
const platform = currentReleasePlatform();
|
|
109
|
+
if (!platform) return;
|
|
110
|
+
const artifact = manifest.artifacts.find((candidate) => candidate?.platform === platform);
|
|
111
|
+
if (!artifact) {
|
|
112
|
+
fail(`bundled manifest is missing current platform artifact ${platform}`);
|
|
113
|
+
}
|
|
114
|
+
const archiveName = artifact.archive ?? artifact.name;
|
|
115
|
+
const archivePath = path.join(releaseRoot, archiveName);
|
|
116
|
+
const tmpRoot = await mkdtemp(path.join(tmpdir(), 'starkscan-cli-contract-'));
|
|
117
|
+
try {
|
|
118
|
+
const listingResult = spawnSync('tar', ['-tzf', archivePath], {
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
env: publicVerifierSpawnEnv(),
|
|
121
|
+
timeout: 15_000,
|
|
122
|
+
});
|
|
123
|
+
if (listingResult.status !== 0 || listingResult.error) {
|
|
124
|
+
fail(`failed to inspect current-platform CLI artifact ${archiveName}: ${describeSpawn(listingResult)}`);
|
|
125
|
+
}
|
|
126
|
+
const listing = listingResult.stdout
|
|
127
|
+
.split(/\r?\n/)
|
|
128
|
+
.map((entry) => entry.trim())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
if (listing.length !== 1 || listing[0] !== PUBLIC_BINARY_NAME) {
|
|
131
|
+
fail(
|
|
132
|
+
`current-platform CLI artifact ${archiveName} must contain exactly one top-level Starkscan binary; found ${JSON.stringify(listing)}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const tarResult = spawnSync('tar', ['-C', tmpRoot, '-xzf', archivePath], {
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
env: publicVerifierSpawnEnv(),
|
|
139
|
+
timeout: 15_000,
|
|
140
|
+
});
|
|
141
|
+
if (tarResult.status !== 0 || tarResult.error) {
|
|
142
|
+
fail(`failed to extract current-platform CLI artifact ${archiveName}: ${describeSpawn(tarResult)}`);
|
|
143
|
+
}
|
|
144
|
+
const binaryPath = path.join(tmpRoot, PUBLIC_BINARY_NAME);
|
|
145
|
+
await assertFile(
|
|
146
|
+
binaryPath,
|
|
147
|
+
'current-platform bundled binary',
|
|
148
|
+
'Archive must contain a root starkscan binary.',
|
|
149
|
+
);
|
|
150
|
+
const helpResult = spawnSync(binaryPath, ['--help'], {
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
env: publicVerifierSpawnEnv(),
|
|
153
|
+
timeout: 15_000,
|
|
154
|
+
});
|
|
155
|
+
if (helpResult.status !== 0 || helpResult.error) {
|
|
156
|
+
fail(
|
|
157
|
+
`current-platform bundled binary failed --help: ${describeSpawn(helpResult)}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const output = `${helpResult.stdout}\n${helpResult.stderr}`;
|
|
161
|
+
if (!output.includes(HOSTED_DEFAULT_BASE_URL)) {
|
|
162
|
+
fail(`current-platform bundled binary help does not advertise hosted default ${HOSTED_DEFAULT_BASE_URL}`);
|
|
163
|
+
}
|
|
164
|
+
for (const forbidden of PUBLIC_HELP_FORBIDDEN_STRINGS) {
|
|
165
|
+
if (output.includes(forbidden)) {
|
|
166
|
+
fail(`current-platform bundled binary help leaks forbidden public contract string: ${forbidden}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} finally {
|
|
170
|
+
await rm(tmpRoot, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
54
174
|
async function main() {
|
|
55
175
|
const packageJson = JSON.parse(await readFile(PACKAGE_JSON_PATH, 'utf8'));
|
|
56
176
|
if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
|
|
@@ -68,19 +188,8 @@ async function main() {
|
|
|
68
188
|
`Expected bundled artifacts under ${releaseRoot}. ` +
|
|
69
189
|
'Run native CLI packaging for every supported platform, merge the manifest, then run ' +
|
|
70
190
|
'`./webapp/packages/cli/scripts/package-starkscan-cli-npm.sh .artifacts/release/npm-cli` from the repository root.';
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const candidate = path.join(releaseRoot, manifestFilename);
|
|
74
|
-
const stat = await lstat(candidate).catch(() => null);
|
|
75
|
-
if (stat?.isFile()) {
|
|
76
|
-
manifestPath = candidate;
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (!manifestPath) {
|
|
81
|
-
manifestPath = path.join(releaseRoot, PUBLIC_MANIFEST_FILENAME);
|
|
82
|
-
await assertFile(manifestPath, 'bundled manifest', remediation);
|
|
83
|
-
}
|
|
191
|
+
const manifestPath = path.join(releaseRoot, PUBLIC_MANIFEST_FILENAME);
|
|
192
|
+
await assertFile(manifestPath, 'bundled manifest', remediation);
|
|
84
193
|
|
|
85
194
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
86
195
|
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
@@ -91,7 +200,7 @@ async function main() {
|
|
|
91
200
|
}
|
|
92
201
|
|
|
93
202
|
for (const platform of SUPPORTED_PLATFORMS) {
|
|
94
|
-
const
|
|
203
|
+
const expectedArchive = `starkscan-${platform}.tar.gz`;
|
|
95
204
|
const matches = manifest.artifacts.filter((artifact) => artifact?.platform === platform);
|
|
96
205
|
if (matches.length !== 1) {
|
|
97
206
|
fail(`manifest must contain exactly one artifact for ${platform}; found ${matches.length}`);
|
|
@@ -101,8 +210,8 @@ async function main() {
|
|
|
101
210
|
if (!archiveName) {
|
|
102
211
|
fail(`artifact for ${platform} must define archive or name`);
|
|
103
212
|
}
|
|
104
|
-
if (
|
|
105
|
-
fail(`artifact for ${platform} must be named
|
|
213
|
+
if (archiveName !== expectedArchive) {
|
|
214
|
+
fail(`artifact for ${platform} must be named ${expectedArchive}`);
|
|
106
215
|
}
|
|
107
216
|
if (artifact.archive !== undefined && artifact.name !== undefined && artifact.archive !== artifact.name) {
|
|
108
217
|
fail(`artifact archive and name for ${platform} must match`);
|
|
@@ -126,6 +235,8 @@ async function main() {
|
|
|
126
235
|
fail(`${archiveName} bytes do not match manifest sha256`);
|
|
127
236
|
}
|
|
128
237
|
}
|
|
238
|
+
|
|
239
|
+
await verifyCurrentPlatformHelpContract(releaseRoot, manifest);
|
|
129
240
|
}
|
|
130
241
|
|
|
131
242
|
main().catch((error) => {
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
26e55c13e3b8665ba058e0032007c8277ce187dc5710d8ac4e5e50686a46f00a starkscan-darwin-aarch64.tar.gz
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
46d03132446a46b54dcb9f529c2547a9c1c0f7615d5022c70caceea0ba1be014 starkscan-darwin-x86_64.tar.gz
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
8b3a1febf6ed7c4469a2703f1e29bc64a1d28bc6af538f3c80e6d739ea89c30d starkscan-linux-aarch64.tar.gz
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
4d48f2ecb220961574577b5b20c3272fddbd5c35aa4972391b8de718da85f807 starkscan-linux-x86_64.tar.gz
|