Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { templateRoutes } from './routes/templates.js';
import { qrRoutes } from './routes/qr.js';
import { wellKnownRoutes } from './routes/well-known.js';

/**
* Configuration options for creating a LinkForty server instance.
*/
export interface ServerOptions {
database?: DatabaseOptions;
redis?: {
Expand All @@ -22,6 +25,15 @@ export interface ServerOptions {
logger?: boolean;
}

/**
* Create and configure a LinkForty Fastify server instance.
*
* Registers CORS, optional Redis, the database connection, and all built-in
* route plugins. The returned instance is ready to call `listen()` on.
*
* @param options - Server configuration (database, Redis, CORS, logger).
* @returns A configured Fastify instance with all routes registered.
*/
export async function createServer(options: ServerOptions = {}) {
const fastify = Fastify({
logger: options.logger !== undefined ? options.logger : true,
Expand Down
40 changes: 40 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@ import { nanoid } from 'nanoid';
import geoip from 'geoip-lite';
import UAParser from 'ua-parser-js';

/**
* Generate a URL-safe random short code using nanoid.
*
* @param length - Number of characters in the generated code. Defaults to 8.
* @returns A random URL-safe string of the specified length.
*/
export function generateShortCode(length: number = 8): string {
return nanoid(length);
}

/**
* Parse a User-Agent string into structured device and browser information.
*
* @param userAgent - Raw User-Agent header value from an HTTP request.
* @returns An object containing `deviceType`, `platform`, `platformVersion`, and `browser`.
*/
export function parseUserAgent(userAgent: string) {
const parser = new UAParser(userAgent);
const result = parser.getResult();
Expand Down Expand Up @@ -68,6 +80,14 @@ const COUNTRY_NAMES: Record<string, string> = {
RO: 'Romania',
};

/**
* Look up geographic location data for an IP address using geoip-lite.
*
* @param ip - IPv4 or IPv6 address to look up.
* @returns An object with `countryCode`, `countryName`, `region`, `city`,
* `latitude`, `longitude`, and `timezone`. All fields are `null` when the
* IP address is not found in the GeoIP database.
*/
export function getLocationFromIP(ip: string) {
const geo = geoip.lookup(ip);

Expand All @@ -94,6 +114,17 @@ export function getLocationFromIP(ip: string) {
};
}

/**
* Append UTM tracking parameters to a URL.
*
* Each key in `utmParameters` is prefixed with `utm_` before being added as a
* query parameter (e.g., `{ source: 'email' }` → `?utm_source=email`).
* Empty values are skipped.
*
* @param originalUrl - The destination URL to append parameters to.
* @param utmParameters - Optional map of UTM parameter names (without the `utm_` prefix) to values.
* @returns The URL string with UTM parameters appended.
*/
export function buildRedirectUrl(
originalUrl: string,
utmParameters?: Record<string, string>
Expand All @@ -111,6 +142,15 @@ export function buildRedirectUrl(
return url.toString();
}

/**
* Detect the device platform from a User-Agent string.
*
* Uses simple substring matching to identify iOS and Android devices.
* Anything that does not match is classified as `'web'`.
*
* @param userAgent - Raw User-Agent header value from an HTTP request.
* @returns `'ios'`, `'android'`, or `'web'`.
*/
export function detectDevice(userAgent: string): 'ios' | 'android' | 'web' {
const ua = userAgent.toLowerCase();

Expand Down
44 changes: 44 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// Template types

/**
* Default settings applied to links created from a template.
*/
export interface LinkTemplateSettings {
defaultIosUrl?: string;
defaultAndroidUrl?: string;
Expand All @@ -9,6 +13,9 @@ export interface LinkTemplateSettings {
expiresAfterDays?: number;
}

/**
* A reusable link template that pre-populates settings when creating new links.
*/
export interface LinkTemplate {
id: string;
userId?: string;
Expand All @@ -30,6 +37,9 @@ export interface CreateTemplateRequest {

export interface UpdateTemplateRequest extends Partial<CreateTemplateRequest> {}

/**
* A short link with routing, deep-linking, UTM, targeting, and Open Graph metadata.
*/
export interface Link {
id: string;
userId?: string;
Expand Down Expand Up @@ -64,6 +74,9 @@ export interface Link {
click_count?: number;
}

/**
* Standard UTM tracking parameters appended to redirect URLs for campaign attribution.
*/
export interface UTMParameters {
source?: string;
medium?: string;
Expand All @@ -72,12 +85,19 @@ export interface UTMParameters {
content?: string;
}

/**
* Rules that control which redirect URL a visitor receives based on their
* country, device type, or browser language.
*/
export interface TargetingRules {
countries?: string[];
devices?: ('ios' | 'android' | 'web')[];
languages?: string[];
}

/**
* A recorded click event on a short link, including device, location, and UTM data.
*/
export interface ClickEvent {
id: string;
linkId: string;
Expand Down Expand Up @@ -130,6 +150,10 @@ export interface UpdateLinkRequest extends Partial<CreateLinkRequest> {
isActive?: boolean;
}

/**
* Aggregated analytics for one or more links over a time period, broken down
* by date, geography, device, browser, UTM parameters, and referrer.
*/
export interface AnalyticsData {
totalClicks: number;
uniqueClicks: number;
Expand Down Expand Up @@ -157,8 +181,16 @@ export interface AnalyticsData {
}

// Webhook types

/**
* Discriminated event type sent in webhook payloads.
* Consumers should filter webhooks by subscribing to specific event types.
*/
export type WebhookEvent = 'click_event' | 'install_event' | 'conversion_event' | 'sdk_event';

/**
* A registered webhook endpoint that receives event notifications from LinkForty.
*/
export interface Webhook {
id: string;
user_id: string;
Expand Down Expand Up @@ -193,13 +225,19 @@ export interface UpdateWebhookRequest {
timeoutMs?: number;
}

/**
* The JSON body delivered to a webhook endpoint for every event.
*/
export interface WebhookPayload {
event: WebhookEvent;
event_id: string;
timestamp: string;
data: ClickEvent | InstallEvent | ConversionEvent;
}

/**
* Outcome of a single webhook delivery attempt, including HTTP status and retry info.
*/
export interface WebhookDeliveryResult {
success: boolean;
webhookId: string;
Expand All @@ -212,6 +250,9 @@ export interface WebhookDeliveryResult {
errorMessage?: string;
}

/**
* An app install event, optionally attributed to a prior click via device fingerprinting.
*/
export interface InstallEvent {
id: string;
linkId?: string;
Expand All @@ -224,6 +265,9 @@ export interface InstallEvent {
platform?: string;
}

/**
* A post-install in-app conversion event (e.g., purchase, sign-up) tied to an install.
*/
export interface ConversionEvent {
id: string;
installId: string;
Expand Down
Loading