uac-package 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ /** Production UAC backend (paths like /api/domains are appended by the client). */
2
+ const DEFAULT_UAC_API_URL = 'https://uac-backend.clay.in';
3
+
4
+ function normalizeApiBaseUrl(url) {
5
+ let value = String(url || '').trim().replace(/\/$/, '');
6
+
7
+ if (!value) {
8
+ return DEFAULT_UAC_API_URL;
9
+ }
10
+
11
+ // Accept https://host/api — strip trailing /api to avoid /api/api/...
12
+ if (value.endsWith('/api')) {
13
+ value = value.slice(0, -4);
14
+ }
15
+
16
+ return value;
17
+ }
18
+
19
+ module.exports = {
20
+ DEFAULT_UAC_API_URL,
21
+ normalizeApiBaseUrl,
22
+ };
@@ -0,0 +1,15 @@
1
+ export const DEFAULT_UAC_API_URL = 'https://uac-backend.clay.in';
2
+
3
+ export function normalizeApiBaseUrl(url) {
4
+ let value = String(url || '').trim().replace(/\/$/, '');
5
+
6
+ if (!value) {
7
+ return DEFAULT_UAC_API_URL;
8
+ }
9
+
10
+ if (value.endsWith('/api')) {
11
+ value = value.slice(0, -4);
12
+ }
13
+
14
+ return value;
15
+ }
@@ -0,0 +1,56 @@
1
+ const { init } = require('./client');
2
+ const { resolveAppUrl, normalizeDomain } = require('./normalizeDomain');
3
+
4
+ const trackedDomains = new Set();
5
+
6
+ function isAutoTrackEnabled() {
7
+ return process.env.UAC_AUTO_TRACK !== 'false';
8
+ }
9
+
10
+ function canAutoTrack(options = {}) {
11
+ if (options.url || options.domain || options.name) return true;
12
+ if (options.port || process.env.PORT) return true;
13
+ return false;
14
+ }
15
+
16
+ function autoTrack(options = {}) {
17
+ if (!isAutoTrackEnabled()) {
18
+ return Promise.resolve(null);
19
+ }
20
+
21
+ const config = { ...options };
22
+
23
+ if (!canAutoTrack(config)) {
24
+ return Promise.resolve(null);
25
+ }
26
+
27
+ const appUrl = resolveAppUrl(config);
28
+ const domain = normalizeDomain(appUrl);
29
+
30
+ if (!domain) {
31
+ return Promise.resolve(null);
32
+ }
33
+
34
+ if (trackedDomains.has(domain)) {
35
+ return Promise.resolve({ created: false, name: domain, message: 'Already tracked' });
36
+ }
37
+
38
+ console.log('[UAC] Auto-tracking domain:', domain);
39
+
40
+ return init(config)
41
+ .then((registration) => {
42
+ trackedDomains.add(registration.name);
43
+ if (registration.created) {
44
+ console.log('[UAC] Domain stored:', registration.name);
45
+ } else {
46
+ console.log('[UAC] Domain already registered:', registration.name);
47
+ }
48
+ return registration;
49
+ })
50
+ .catch((error) => {
51
+ console.warn('[UAC] Auto-track failed:', error.message);
52
+ return null;
53
+ });
54
+ }
55
+
56
+ module.exports = { autoTrack, isAutoTrackEnabled, canAutoTrack };
package/lib/boot.js ADDED
@@ -0,0 +1,33 @@
1
+ const { normalizeDomain, resolveAppUrl } = require('./normalizeDomain');
2
+ const { getApiUrl, init } = require('./client');
3
+
4
+ async function boot(options = {}) {
5
+ const port = options.port || process.env.PORT || 4001;
6
+ const config = { ...options, port };
7
+ const appUrl = resolveAppUrl(config);
8
+ const apiUrl = getApiUrl(config);
9
+ const domain = normalizeDomain(appUrl);
10
+
11
+ console.log('UAC: Registering domain...');
12
+ console.log('UAC: App URL:', appUrl);
13
+ console.log('UAC: Backend:', apiUrl);
14
+ console.log('UAC: Domain:', domain);
15
+
16
+ const registration = await init(config);
17
+
18
+ if (registration.created) {
19
+ console.log('UAC: Domain stored in database:', registration.name);
20
+ } else {
21
+ console.log('UAC: Domain already in database:', registration.name);
22
+ }
23
+
24
+ return {
25
+ port,
26
+ appUrl,
27
+ domain,
28
+ registration,
29
+ apiUrl,
30
+ };
31
+ }
32
+
33
+ module.exports = { boot };
package/lib/client.js ADDED
@@ -0,0 +1,85 @@
1
+ const {
2
+ normalizeDomain,
3
+ isValidDomain,
4
+ resolveAppUrl,
5
+ } = require('./normalizeDomain');
6
+ const { DEFAULT_UAC_API_URL, normalizeApiBaseUrl } = require('./apiConfig');
7
+
8
+ class UacError extends Error {
9
+ constructor(message, { status, code } = {}) {
10
+ super(message);
11
+ this.name = 'UacError';
12
+ this.status = status;
13
+ this.code = code;
14
+ }
15
+ }
16
+
17
+ function getApiUrl(options = {}) {
18
+ return normalizeApiBaseUrl(
19
+ options.apiUrl || process.env.UAC_API_URL || DEFAULT_UAC_API_URL
20
+ );
21
+ }
22
+
23
+ async function registerDomain(input, options = {}) {
24
+ const name = normalizeDomain(input);
25
+
26
+ if (!name) {
27
+ throw new UacError('Domain or URL is required', { code: 'INVALID_INPUT' });
28
+ }
29
+
30
+ if (!isValidDomain(name)) {
31
+ throw new UacError(`Invalid domain: ${name}`, { code: 'INVALID_DOMAIN' });
32
+ }
33
+
34
+ const apiUrl = getApiUrl(options);
35
+ const response = await fetch(`${apiUrl}/api/domains`, {
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ ...(options.headers || {}),
40
+ },
41
+ body: JSON.stringify({ name }),
42
+ });
43
+
44
+ const data = await response.json().catch(() => ({}));
45
+
46
+ if (response.status === 201) {
47
+ return {
48
+ created: true,
49
+ domain: data,
50
+ name: data.name,
51
+ };
52
+ }
53
+
54
+ if (response.status === 409) {
55
+ return {
56
+ created: false,
57
+ name,
58
+ message: 'Domain already registered',
59
+ };
60
+ }
61
+
62
+ throw new UacError(data.error || 'Failed to register domain', {
63
+ status: response.status,
64
+ code: 'REGISTER_FAILED',
65
+ });
66
+ }
67
+
68
+ async function init(input, options = {}) {
69
+ const config =
70
+ input && typeof input === 'object' && !Array.isArray(input)
71
+ ? input
72
+ : { ...(input ? { url: input } : {}), ...options };
73
+
74
+ const source = resolveAppUrl(config);
75
+
76
+ return registerDomain(source, config);
77
+ }
78
+
79
+ module.exports = {
80
+ UacError,
81
+ getApiUrl,
82
+ registerDomain,
83
+ init,
84
+ resolveAppUrl,
85
+ };
package/lib/device.mjs ADDED
@@ -0,0 +1,67 @@
1
+ const DEVICE_STORAGE_KEY = 'uac_device_id';
2
+
3
+ function detectPlatform() {
4
+ const ua = navigator.userAgent || '';
5
+ const platform = navigator.userAgentData?.platform || navigator.platform || '';
6
+
7
+ if (/android/i.test(ua)) return 'mobile';
8
+ if (/iphone|ipad|ipod/i.test(ua)) return 'mobile';
9
+ if (/win/i.test(platform) || /windows/i.test(ua)) return 'windows';
10
+ if (/mac/i.test(platform) || /macintosh/i.test(ua)) return 'mac';
11
+ if (/linux/i.test(platform) || /linux/i.test(ua)) return 'linux';
12
+ if (/mobile/i.test(ua)) return 'mobile';
13
+
14
+ return 'unknown';
15
+ }
16
+
17
+ function getDeviceId() {
18
+ let deviceId = localStorage.getItem(DEVICE_STORAGE_KEY);
19
+
20
+ if (!deviceId) {
21
+ deviceId =
22
+ typeof crypto !== 'undefined' && crypto.randomUUID
23
+ ? crypto.randomUUID()
24
+ : `uac-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
25
+
26
+ localStorage.setItem(DEVICE_STORAGE_KEY, deviceId);
27
+ }
28
+
29
+ return deviceId;
30
+ }
31
+
32
+ function getDeviceInfo() {
33
+ const timezone = Intl.DateTimeFormat().resolvedOptions();
34
+ const platform = detectPlatform();
35
+ const osLabel =
36
+ platform === 'windows'
37
+ ? 'Windows'
38
+ : platform === 'mac'
39
+ ? 'Mac'
40
+ : platform === 'linux'
41
+ ? 'Linux'
42
+ : platform === 'mobile'
43
+ ? 'Mobile'
44
+ : 'Unknown';
45
+
46
+ return {
47
+ deviceType: platform,
48
+ deviceName: navigator.userAgent.slice(0, 200),
49
+ deviceModel: navigator.platform || 'unknown',
50
+ deviceVersion: navigator.appVersion || 'unknown',
51
+ deviceManufacturer: navigator.vendor || 'unknown',
52
+ deviceOperatingSystem: osLabel,
53
+ deviceOperatingSystemVersion: navigator.userAgent.slice(0, 120),
54
+ deviceOperatingSystemArchitecture:
55
+ navigator.userAgentData?.platform || navigator.platform || 'unknown',
56
+ deviceOperatingSystemLanguage: navigator.language || 'unknown',
57
+ deviceOperatingSystemCountry: navigator.language?.split('-')[1] || 'unknown',
58
+ deviceOperatingSystemCity: 'unknown',
59
+ deviceOperatingSystemRegion: 'unknown',
60
+ deviceOperatingSystemPostalCode: 'unknown',
61
+ deviceOperatingSystemTimezone: timezone.timeZone || 'unknown',
62
+ deviceOperatingSystemTimezoneOffset: new Date().getTimezoneOffset(),
63
+ deviceOperatingSystemTimezoneName: timezone.timeZone || 'unknown',
64
+ };
65
+ }
66
+
67
+ export { getDeviceId, getDeviceInfo, detectPlatform };
@@ -0,0 +1,120 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const UAC_MARKER = 'uac-package';
5
+ const UAC_AUTO_MARKER = '@uac-auto';
6
+ const TRACK_IMPORT = `import '${UAC_MARKER}/track' // ${UAC_AUTO_MARKER}\n`;
7
+ const ENTRY_FILES = ['src/main.js', 'src/main.ts', 'src/index.js', 'src/index.ts'];
8
+
9
+ function readJson(filePath) {
10
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
11
+ }
12
+
13
+ function writeJson(filePath, data) {
14
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
15
+ }
16
+
17
+ function hasUacDependency(pkg) {
18
+ return Boolean(
19
+ pkg.dependencies?.[UAC_MARKER] || pkg.devDependencies?.[UAC_MARKER]
20
+ );
21
+ }
22
+
23
+ function patchDevScript(script) {
24
+ if (!script || script.includes(UAC_MARKER)) {
25
+ return script;
26
+ }
27
+
28
+ return `node ./node_modules/${UAC_MARKER}/bin/cli.js dev ${script}`;
29
+ }
30
+
31
+ function patchNodeStartScript(script) {
32
+ if (!script || script.includes(`${UAC_MARKER}/register`)) {
33
+ return script;
34
+ }
35
+
36
+ if (/^node\s+/i.test(script)) {
37
+ return script.replace(/^node\s+/i, `node -r ${UAC_MARKER}/register `);
38
+ }
39
+
40
+ return `node -r ${UAC_MARKER}/register ${script}`;
41
+ }
42
+
43
+ function isViteProject(pkg) {
44
+ return Boolean(pkg.devDependencies?.vite || pkg.dependencies?.vite);
45
+ }
46
+
47
+ function injectClickTracker(projectRoot) {
48
+ for (const relPath of ENTRY_FILES) {
49
+ const filePath = path.join(projectRoot, relPath);
50
+
51
+ if (!fs.existsSync(filePath)) {
52
+ continue;
53
+ }
54
+
55
+ const content = fs.readFileSync(filePath, 'utf8');
56
+
57
+ if (
58
+ content.includes(UAC_AUTO_MARKER) ||
59
+ content.includes(`${UAC_MARKER}/track`)
60
+ ) {
61
+ return false;
62
+ }
63
+
64
+ fs.writeFileSync(filePath, TRACK_IMPORT + content);
65
+ return true;
66
+ }
67
+
68
+ return false;
69
+ }
70
+
71
+ function integrate(projectRoot) {
72
+ const pkgPath = path.join(projectRoot, 'package.json');
73
+
74
+ if (!fs.existsSync(pkgPath)) {
75
+ return false;
76
+ }
77
+
78
+ const pkg = readJson(pkgPath);
79
+
80
+ if (pkg.name === UAC_MARKER || !hasUacDependency(pkg)) {
81
+ return false;
82
+ }
83
+
84
+ let changed = false;
85
+
86
+ if (isViteProject(pkg) && pkg.scripts) {
87
+ for (const name of ['dev', 'preview']) {
88
+ const next = patchDevScript(pkg.scripts[name]);
89
+ if (next && next !== pkg.scripts[name]) {
90
+ pkg.scripts[name] = next;
91
+ changed = true;
92
+ }
93
+ }
94
+ } else if (pkg.scripts?.start) {
95
+ const next = patchNodeStartScript(pkg.scripts.start);
96
+ if (next !== pkg.scripts.start) {
97
+ pkg.scripts.start = next;
98
+ changed = true;
99
+ }
100
+ }
101
+
102
+ if (injectClickTracker(projectRoot)) {
103
+ changed = true;
104
+ }
105
+
106
+ if (changed) {
107
+ writeJson(pkgPath, pkg);
108
+ console.log('[UAC] Auto-integrated — domains & clicks will track automatically');
109
+ }
110
+
111
+ return changed;
112
+ }
113
+
114
+ module.exports = {
115
+ integrate,
116
+ injectClickTracker,
117
+ patchDevScript,
118
+ patchNodeStartScript,
119
+ isViteProject,
120
+ };
@@ -0,0 +1,62 @@
1
+ const DOMAIN_NAME_REGEX =
2
+ /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i;
3
+ const LOCALHOST_REGEX = /^localhost(?::\d+)?$/;
4
+ const IP_REGEX = /^(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?$/;
5
+
6
+ function normalizeDomain(input) {
7
+ let value = String(input || '').trim().toLowerCase();
8
+
9
+ if (!value) {
10
+ return '';
11
+ }
12
+
13
+ if (value.includes('://')) {
14
+ try {
15
+ value = new URL(value).host;
16
+ } catch {
17
+ value = value.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
18
+ }
19
+ } else {
20
+ value = value.replace(/\/.*$/, '');
21
+ }
22
+
23
+ return value.replace(/^www\./, '');
24
+ }
25
+
26
+ function isValidDomain(name) {
27
+ return (
28
+ DOMAIN_NAME_REGEX.test(name) ||
29
+ LOCALHOST_REGEX.test(name) ||
30
+ IP_REGEX.test(name)
31
+ );
32
+ }
33
+
34
+ function resolveAppUrl(options = {}) {
35
+ if (options.url) {
36
+ return options.url;
37
+ }
38
+
39
+ if (options.domain) {
40
+ return options.domain;
41
+ }
42
+
43
+ if (options.name) {
44
+ return options.name;
45
+ }
46
+
47
+ const host = options.host || process.env.HOST || 'localhost';
48
+ const port = options.port || process.env.PORT;
49
+ const protocol = options.protocol || process.env.APP_PROTOCOL || 'http';
50
+
51
+ if (port) {
52
+ return `${protocol}://${host}:${port}`;
53
+ }
54
+
55
+ return `${protocol}://${host}`;
56
+ }
57
+
58
+ module.exports = {
59
+ normalizeDomain,
60
+ isValidDomain,
61
+ resolveAppUrl,
62
+ };
@@ -0,0 +1 @@
1
+ require('./autoTrack').autoTrack();
package/lib/start.js ADDED
@@ -0,0 +1,34 @@
1
+ const http = require('http');
2
+ const { boot } = require('./boot');
3
+
4
+ async function start(options = {}) {
5
+ const result = await boot(options);
6
+ const { port, appUrl, domain, registration } = result;
7
+
8
+ const server = http.createServer((_req, res) => {
9
+ res.writeHead(200, { 'Content-Type': 'application/json' });
10
+ res.end(
11
+ JSON.stringify(
12
+ {
13
+ framework: 'plain-node',
14
+ message: 'App running with uac-package',
15
+ appUrl,
16
+ domain,
17
+ registration,
18
+ },
19
+ null,
20
+ 2
21
+ )
22
+ );
23
+ });
24
+
25
+ return new Promise((resolve, reject) => {
26
+ server.listen(port, () => {
27
+ console.log(`UAC: App running at http://localhost:${port}`);
28
+ resolve({ server, ...result });
29
+ });
30
+ server.on('error', reject);
31
+ });
32
+ }
33
+
34
+ module.exports = { start };
package/lib/track.mjs ADDED
@@ -0,0 +1,147 @@
1
+ import { getDeviceId, getDeviceInfo } from './device.mjs';
2
+ import { DEFAULT_UAC_API_URL, normalizeApiBaseUrl } from './apiConfig.mjs';
3
+
4
+ const pageStart = Date.now();
5
+
6
+ function getApiUrl() {
7
+ if (typeof import.meta !== 'undefined' && import.meta.env?.VITE_UAC_API_URL) {
8
+ return normalizeApiBaseUrl(import.meta.env.VITE_UAC_API_URL);
9
+ }
10
+
11
+ return DEFAULT_UAC_API_URL;
12
+ }
13
+
14
+ function isTrackingEnabled() {
15
+ if (typeof import.meta !== 'undefined' && import.meta.env?.VITE_UAC_AUTO_TRACK === 'false') {
16
+ return false;
17
+ }
18
+
19
+ return true;
20
+ }
21
+
22
+ function getElementLabel(element) {
23
+ const tag = element.tagName?.toLowerCase() || 'unknown';
24
+ const id = element.id ? `#${element.id}` : '';
25
+ const className =
26
+ typeof element.className === 'string' && element.className
27
+ ? `.${element.className.trim().split(/\s+/)[0]}`
28
+ : '';
29
+
30
+ return `${tag}${id}${className}`;
31
+ }
32
+
33
+ function getClickName(element) {
34
+ return `click:${element.tagName?.toLowerCase() || 'element'}`;
35
+ }
36
+
37
+ function getClickDescription(element) {
38
+ const text = (element.innerText || element.textContent || '')
39
+ .trim()
40
+ .replace(/\s+/g, ' ')
41
+ .slice(0, 120);
42
+
43
+ return [text, getElementLabel(element), window.location.pathname]
44
+ .filter(Boolean)
45
+ .join(' | ');
46
+ }
47
+
48
+ async function sendHeartbeat() {
49
+ if (!isTrackingEnabled() || typeof document === 'undefined') {
50
+ return;
51
+ }
52
+
53
+ if (document.visibilityState === 'hidden') {
54
+ return;
55
+ }
56
+
57
+ const payload = {
58
+ domain: window.location.host,
59
+ deviceId: getDeviceId(),
60
+ device: getDeviceInfo(),
61
+ route: window.location.pathname,
62
+ };
63
+
64
+ try {
65
+ await fetch(`${getApiUrl()}/api/presence/heartbeat`, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify(payload),
69
+ keepalive: true,
70
+ });
71
+ } catch {
72
+ // Backend may be offline during local dev
73
+ }
74
+ }
75
+
76
+ async function trackClick(event) {
77
+ const target = event.target;
78
+ if (!(target instanceof Element)) {
79
+ return;
80
+ }
81
+
82
+ const clickedAt = new Date().toISOString();
83
+ const payload = {
84
+ domain: window.location.host,
85
+ deviceId: getDeviceId(),
86
+ device: getDeviceInfo(),
87
+ name: getClickName(target),
88
+ description: getClickDescription(target),
89
+ spentTime: Date.now() - pageStart,
90
+ clickedAt,
91
+ };
92
+
93
+ try {
94
+ await fetch(`${getApiUrl()}/api/activities`, {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify(payload),
98
+ keepalive: true,
99
+ });
100
+ sendHeartbeat();
101
+ } catch {
102
+ // Backend may be offline during local dev
103
+ }
104
+ }
105
+
106
+ function listenForRouteChanges() {
107
+ if (typeof window === 'undefined') return;
108
+
109
+ const notify = () => sendHeartbeat();
110
+
111
+ window.addEventListener('popstate', notify);
112
+
113
+ for (const method of ['pushState', 'replaceState']) {
114
+ const original = history[method].bind(history);
115
+ history[method] = (...args) => {
116
+ const result = original(...args);
117
+ notify();
118
+ return result;
119
+ };
120
+ }
121
+ }
122
+
123
+ function startHeartbeat() {
124
+ sendHeartbeat();
125
+ setInterval(sendHeartbeat, 30000);
126
+ listenForRouteChanges();
127
+
128
+ document.addEventListener('visibilitychange', () => {
129
+ if (document.visibilityState === 'visible') {
130
+ sendHeartbeat();
131
+ }
132
+ });
133
+ }
134
+
135
+ function startClickTracking() {
136
+ if (typeof window === 'undefined' || !isTrackingEnabled()) {
137
+ return;
138
+ }
139
+
140
+ getDeviceId();
141
+ document.addEventListener('click', trackClick, true);
142
+ startHeartbeat();
143
+ }
144
+
145
+ startClickTracking();
146
+
147
+ export { startClickTracking, trackClick, sendHeartbeat };
package/lib/vite.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { UacOptions } from '../index.d.ts';
3
+
4
+ export function vitePlugin(options?: UacOptions): Plugin;
5
+ export default function uacVite(options?: UacOptions): Plugin;
package/lib/vite.js ADDED
@@ -0,0 +1,28 @@
1
+ const { autoTrack } = require('./autoTrack');
2
+
3
+ function resolveServerHost(server) {
4
+ const host = server.config.server?.host;
5
+ if (typeof host === 'string' && host !== 'true') {
6
+ return host;
7
+ }
8
+ return 'localhost';
9
+ }
10
+
11
+ function vitePlugin(options = {}) {
12
+ return {
13
+ name: 'uac-package',
14
+ configureServer(server) {
15
+ server.httpServer?.once('listening', () => {
16
+ const address = server.httpServer?.address();
17
+ const port =
18
+ typeof address === 'object' && address?.port
19
+ ? address.port
20
+ : server.config.server?.port ?? 5173;
21
+
22
+ autoTrack({ ...options, port, host: resolveServerHost(server) });
23
+ });
24
+ },
25
+ };
26
+ }
27
+
28
+ module.exports = { vitePlugin, default: vitePlugin };
package/lib/vite.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const { vitePlugin } = require('./vite.js');
5
+
6
+ export default function uacVite(options = {}) {
7
+ return vitePlugin(options);
8
+ }
9
+
10
+ export { vitePlugin };