@tracetail/angular
Version:
Angular SDK for TraceTail browser fingerprinting - over 99.5% accuracy
404 lines (393 loc) • 15.6 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, Inject, Injectable, Input, Directive, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';
import TraceTail from '@tracetail/js';
import { take, switchMap, map } from 'rxjs/operators';
import * as i2 from '@angular/router';
const TRACETAIL_CONFIG = new InjectionToken('TRACETAIL_CONFIG');
const DEFAULT_CONFIG = {
config: {
enhanced: true,
timeout: 5000,
endpoint: 'https://api.tracetail.com',
testMode: false
}
};
class TraceTailService {
config;
client;
fingerprintSubject = new BehaviorSubject(null);
loadingSubject = new BehaviorSubject(true);
errorSubject = new BehaviorSubject(null);
fingerprint$ = this.fingerprintSubject.asObservable();
loading$ = this.loadingSubject.asObservable();
error$ = this.errorSubject.asObservable();
constructor(config) {
this.config = config;
this.initializeClient();
}
initializeClient() {
try {
// Handle test mode
if (this.config.config?.testMode && this.config.config.mockData) {
this.fingerprintSubject.next({
visitorId: 'mock-visitor-123',
confidence: 0.99,
riskScore: 0.1,
fraudulent: false,
signals: {},
timestamp: new Date(),
...this.config.config.mockData
});
this.loadingSubject.next(false);
return;
}
// Initialize real client
this.client = new TraceTail({
apiKey: this.config.apiKey,
timeout: this.config.config?.timeout,
debug: this.config.config?.debug || false
});
// Get initial fingerprint
this.loadFingerprint();
}
catch (error) {
this.handleError(error);
}
}
async loadFingerprint() {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
const result = await this.client.generateFingerprint();
const fingerprint = {
visitorId: result.visitorId,
confidence: result.confidence,
riskScore: 0,
fraudulent: false,
signals: result.components || {},
timestamp: new Date()
};
this.fingerprintSubject.next(fingerprint);
this.loadingSubject.next(false);
}
catch (error) {
this.handleError(error);
}
}
handleError(error) {
console.error('TraceTail error:', error);
this.errorSubject.next(error);
this.loadingSubject.next(false);
}
/**
* Get current fingerprint as a promise
*/
async getFingerprint() {
const current = this.fingerprintSubject.value;
if (current) {
return current;
}
// Wait for fingerprint to be loaded
return new Promise((resolve, reject) => {
const subscription = this.fingerprint$.subscribe({
next: (fingerprint) => {
if (fingerprint) {
subscription.unsubscribe();
resolve(fingerprint);
}
},
error: (error) => {
subscription.unsubscribe();
reject(error);
}
});
});
}
/**
* Track an event
*/
async trackEvent(event, data) {
try {
// TraceTail doesn't have a track method - this is custom app functionality
// For now, just return mock data with a generated event ID
const eventId = `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
return {
success: true,
fraudulent: false,
riskScore: 0,
eventId: eventId
};
}
catch (error) {
console.error('Track event error:', error);
throw error;
}
}
/**
* Check for fraud
*/
async checkFraud(data) {
try {
const fingerprint = await this.getFingerprint();
// Simple fraud detection logic based on risk score
const block = fingerprint.riskScore > 0.8;
const challenge = fingerprint.riskScore > 0.5 && fingerprint.riskScore <= 0.8;
const allow = fingerprint.riskScore <= 0.5;
const reasons = [];
if (fingerprint.signals.vpn)
reasons.push('VPN detected');
if (fingerprint.signals.tor)
reasons.push('Tor browser detected');
if (fingerprint.signals.proxy)
reasons.push('Proxy detected');
if (fingerprint.riskScore > 0.7)
reasons.push('High risk score');
return {
block,
challenge,
allow,
riskScore: fingerprint.riskScore,
reasons
};
}
catch (error) {
console.error('Fraud check error:', error);
throw error;
}
}
/**
* Retry fingerprinting
*/
retry() {
this.loadFingerprint();
}
/**
* Force refresh fingerprint
*/
async refresh() {
await this.loadFingerprint();
}
/**
* Get raw TraceTail client instance
*/
getClient() {
return this.client;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TraceTailService, deps: [{ token: TRACETAIL_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TraceTailService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TraceTailService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [TRACETAIL_CONFIG]
}] }] });
class FraudDetectionDirective {
el;
traceTail;
appFraudDetection = 'medium';
disableOnRisk = true;
cssClass = 'high-risk';
subscription = null;
constructor(el, traceTail) {
this.el = el;
this.traceTail = traceTail;
}
ngOnInit() {
this.subscription = this.traceTail.fingerprint$.subscribe(fingerprint => {
if (!fingerprint)
return;
const thresholds = {
low: 0.3,
medium: 0.5,
high: 0.7
};
const threshold = thresholds[this.appFraudDetection];
const isHighRisk = fingerprint.riskScore > threshold;
// Apply or remove CSS class
if (isHighRisk) {
this.el.nativeElement.classList.add(this.cssClass);
// Optionally disable the element
if (this.disableOnRisk && this.isFormElement()) {
this.el.nativeElement.setAttribute('disabled', 'true');
this.el.nativeElement.setAttribute('title', 'High fraud risk detected');
}
}
else {
this.el.nativeElement.classList.remove(this.cssClass);
if (this.disableOnRisk && this.isFormElement()) {
this.el.nativeElement.removeAttribute('disabled');
this.el.nativeElement.removeAttribute('title');
}
}
// Emit custom event
this.el.nativeElement.dispatchEvent(new CustomEvent('fraudStatusChange', {
detail: {
isHighRisk,
riskScore: fingerprint.riskScore,
threshold
},
bubbles: true
}));
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
isFormElement() {
const tagName = this.el.nativeElement.tagName.toLowerCase();
return ['input', 'button', 'select', 'textarea', 'form'].includes(tagName);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FraudDetectionDirective, deps: [{ token: i0.ElementRef }, { token: TraceTailService }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: FraudDetectionDirective, selector: "[appFraudDetection]", inputs: { appFraudDetection: "appFraudDetection", disableOnRisk: "disableOnRisk", cssClass: "cssClass" }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FraudDetectionDirective, decorators: [{
type: Directive,
args: [{
selector: '[appFraudDetection]'
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: TraceTailService }], propDecorators: { appFraudDetection: [{
type: Input
}], disableOnRisk: [{
type: Input
}], cssClass: [{
type: Input
}] } });
class TraceTailInterceptor {
traceTail;
constructor(traceTail) {
this.traceTail = traceTail;
}
intercept(req, next) {
// Skip TraceTail API requests to prevent circular dependency
if (req.url.includes('api.tracetail.com')) {
return next.handle(req);
}
return this.traceTail.fingerprint$.pipe(take(1), switchMap(fingerprint => {
// Clone the request and add TraceTail headers
const modifiedReq = req.clone({
headers: req.headers
.set('X-TraceTail-Visitor-ID', fingerprint?.visitorId || 'unknown')
.set('X-TraceTail-Risk-Score', fingerprint?.riskScore?.toString() || '0')
.set('X-TraceTail-Confidence', fingerprint?.confidence?.toString() || '0')
});
return next.handle(modifiedReq);
}));
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TraceTailInterceptor, deps: [{ token: TraceTailService }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TraceTailInterceptor });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TraceTailInterceptor, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: TraceTailService }] });
class TraceTailModule {
static forRoot(config) {
return {
ngModule: TraceTailModule,
providers: [
{
provide: TRACETAIL_CONFIG,
useValue: { ...DEFAULT_CONFIG, ...config }
},
TraceTailService,
{
provide: HTTP_INTERCEPTORS,
useClass: TraceTailInterceptor,
multi: true
}
]
};
}
static forFeature(config) {
return {
ngModule: TraceTailModule,
providers: config ? [
{
provide: TRACETAIL_CONFIG,
useValue: { ...DEFAULT_CONFIG, ...config }
}
] : []
};
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TraceTailModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: TraceTailModule, declarations: [FraudDetectionDirective], imports: [CommonModule], exports: [FraudDetectionDirective] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TraceTailModule, imports: [CommonModule] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TraceTailModule, decorators: [{
type: NgModule,
args: [{
declarations: [
FraudDetectionDirective
],
imports: [
CommonModule
],
exports: [
FraudDetectionDirective
]
}]
}] });
/**
* TraceTail types for Angular
*/
class FraudProtectionGuard {
traceTail;
router;
constructor(traceTail, router) {
this.traceTail = traceTail;
this.router = router;
}
canActivate(route, state) {
// Get risk threshold from route data or use default
const maxRiskScore = route.data['maxRiskScore'] ?? 0.7;
const verificationUrl = route.data['verificationUrl'] ?? '/verify';
return this.traceTail.fingerprint$.pipe(take(1), map(fingerprint => {
if (!fingerprint) {
// No fingerprint yet, allow but monitor
console.warn('No fingerprint available for fraud check');
return true;
}
// Check if user is fraudulent or high risk
if (fingerprint.fraudulent || fingerprint.riskScore > maxRiskScore) {
console.warn('High risk user blocked:', {
visitorId: fingerprint.visitorId,
riskScore: fingerprint.riskScore,
fraudulent: fingerprint.fraudulent
});
// Redirect to verification page
return this.router.createUrlTree([verificationUrl], {
queryParams: {
reason: fingerprint.fraudulent ? 'fraud' : 'risk',
returnUrl: state.url
}
});
}
// User is safe, allow access
return true;
}));
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FraudProtectionGuard, deps: [{ token: TraceTailService }, { token: i2.Router }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FraudProtectionGuard, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FraudProtectionGuard, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: TraceTailService }, { type: i2.Router }] });
/**
* Public API Surface of @tracetail/angular
*/
/**
* Generated bundle index. Do not edit.
*/
export { DEFAULT_CONFIG, FraudDetectionDirective, FraudProtectionGuard, TRACETAIL_CONFIG, TraceTailInterceptor, TraceTailModule, TraceTailService };
//# sourceMappingURL=tracetail-angular.mjs.map