npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fynlink/sdk

v0.2.0-beta.7

Published

TypeScript SDK for Fynlink

Readme

FynLink TypeScript SDK

Official TypeScript SDK for FynLink.

Use @fynlink/sdk to create and manage short links, read analytics, work with custom domains, and manage team resources from Node.js and TypeScript applications.

Install

npm install @fynlink/sdk

Quick Start

import { Fyn } from '@fynlink/sdk';

const fyn = new Fyn({
  token: process.env.FYN_API_TOKEN!,
  secret: process.env.FYN_SECRET_KEY
});

const link = await fyn.links.create({
  domain: 'go.example.com',
  target: 'https://example.com',
  title: 'Hello from FynLink'
});

console.log(link.shortUrl);

Client Setup

new Fyn(config) accepts the following options:

| Option | Type | Required | Description | | --- | --- | --- | --- | | token | string | Yes | API token used for authenticated requests. | | secret | string | No | Required for encrypted link reads, lists, and updates. | | baseUrl | string | No | Custom API base URL. Defaults to https://api.fyn.link/v1. | | timeoutMs | number | No | Request timeout in milliseconds. Defaults to 10000. | | debug | boolean | No | Enables HTTP and cache diagnostics. Defaults to false. | | retry.maxAttempts | number | No | Maximum retry attempts. Defaults to 2. | | retry.initialDelayMs | number | No | Initial retry delay in milliseconds. Defaults to 1000. | | cache.enabled | boolean | No | Enables response caching. Defaults to true. | | cache.ttlMs | number | No | Cache TTL in milliseconds. Defaults to 300000. | | cache.storage | 'memory' \| 'disk' | No | Cache backend. Defaults to 'disk'. | | cache.path | string | No | Disk cache file path. Defaults to .fynlink-sdk-cache.json. | | cache.warnOnDisk | boolean | No | Emits a disk-cache security warning. Defaults to true. | | defaults.links.create.domain | string | No | Default domain for fyn.links.create(...) when per-request domain is omitted. | | defaults.links.create.safeMode | boolean | No | Default safeMode for fyn.links.create(...). | | defaults.links.create.analytics | AnalyticsMode | No | Default analytics mode for fyn.links.create(...). | | defaults.links.create.privateLink | boolean | No | Default privacy mode for fyn.links.create(...). | | defaults.links.create.expirySeconds | number | No | Default TTL (seconds) for fyn.links.create(...). |

Example:

import { AnalyticsMode, Fyn } from '@fynlink/sdk';

const fyn = new Fyn({
  token: process.env.FYN_API_TOKEN!,
  secret: process.env.FYN_SECRET_KEY,
  baseUrl: process.env.FYN_API_URL,
  timeoutMs: 10_000,
  debug: false,
  retry: {
    maxAttempts: 2,
    initialDelayMs: 1_000
  },
  cache: {
    enabled: true,
    ttlMs: 300_000,
    storage: 'disk',
    path: '.fynlink-sdk-cache.json',
    warnOnDisk: true
  },
  defaults: {
    links: {
      create: {
        domain: 'go.example.com',
        safeMode: true,
        analytics: AnalyticsMode.Full,
        privateLink: false,
        expirySeconds: 86_400
      }
    }
  }
});

Modules

  • fyn.info
  • fyn.links
  • fyn.analytics
  • fyn.domains
  • fyn.members
  • fyn.teams

Links

Create a Link

Important Team defaults set in the web app (such as default domain, private link mode, safe mode, analytics mode, and expiry) are not automatically respected by the SDK when creating links. Set these explicitly per request or configure defaults.links.create during new Fyn(...).

import { AnalyticsMode } from '@fynlink/sdk';

const link = await fyn.links.create({
  domain: 'go.example.com',
  slug: 'launch-2026',
  target: 'https://example.com',
  title: 'Launch Page',
  notes: 'Primary campaign link',
  tags: ['launch', 'campaign'],
  iosTarget: 'https://apps.apple.com/app/id123456789',
  androidTarget: 'https://play.google.com/store/apps/details?id=com.example.app',
  geoTargets: {
    US: 'https://us.example.com',
    IN: 'https://in.example.com'
  },
  password: 'optional-password',
  safeMode: true,
  privateLink: false,
  analytics: AnalyticsMode.Full,
  expirySeconds: 86_400
});

Possible SDK response:

{
  id: 'link_123',
  userId: 'user_99',
  teamId: 'team_7',
  domain: 'go.example.com',
  slug: 'launch-2026',
  shortUrl: 'https://go.example.com/launch-2026',
  target: {
    default: 'https://example.com',
    ios: 'https://apps.apple.com/app/id123456789',
    android: 'https://play.google.com/store/apps/details?id=com.example.app',
    geo: {
      US: 'https://us.example.com',
      IN: 'https://in.example.com'
    },
    options: {
      password: 'optional-password'
    }
  },
  title: 'Launch Page',
  notes: 'Primary campaign link',
  tags: ['launch', 'campaign'],
  isPrivate: false,
  safeMode: true,
  viaApi: true,
  passwordEnabled: true,
  clicks: 0,
  createdAt: '2026-03-20T10:15:30.000000Z',
  updatedAt: '2026-03-20T10:15:30.000000Z',
  expiresAt: '2026-03-21T10:15:30.000000Z',
  tracking: 'full',
  status: 'active'
}

fyn.links.create(input) parameters:

| Field | Type | Required | Description | | --- | --- | --- | --- | | domain | string | Conditionally | Domain used for the short link. Required unless defaults.links.create.domain is configured in new Fyn(...). | | slug | string | No | Custom slug. Auto-generated when omitted. | | target | string | Yes | Default destination URL. | | title | string | No | Human-readable title. | | notes | string | No | Internal notes. | | tags | string[] | No | Arbitrary tags for organization. | | iosTarget | string | No | iOS-specific destination URL. | | androidTarget | string | No | Android-specific destination URL. | | geoTargets | Record<string, string> | No | Country-specific destination URLs keyed by country code. | | password | string | No | Password required to access the link. | | safeMode | boolean | No | Safe mode flag. Falls back to SDK default (true) when omitted. | | privateLink | boolean | No | Marks the link as private. | | analytics | AnalyticsMode | No | Analytics level. Falls back to SDK default (AnalyticsMode.Full) when omitted. | | expirySeconds | number | No | Time to live in seconds. |

Defaulted create behavior example:

import { AnalyticsMode, Fyn } from '@fynlink/sdk';

const fyn = new Fyn({
  token: process.env.FYN_API_TOKEN!,
  defaults: {
    links: {
      create: {
        domain: 'go.example.com',
        safeMode: true,
        analytics: AnalyticsMode.Full,
        privateLink: false,
        expirySeconds: 86_400
      }
    }
  }
});

// domain/safeMode/analytics/privateLink/expirySeconds come from defaults
const link = await fyn.links.create({
  target: 'https://example.com'
});

Available analytics modes:

  • AnalyticsMode.None
  • AnalyticsMode.Full
  • AnalyticsMode.Partial
  • AnalyticsMode.Clicks

Get, List, Update, and Delete

import { AnalyticsMode, LinkStatus } from '@fynlink/sdk';

const link = await fyn.links.get('link_123');

const links = await fyn.links.list({
  page: 1,
  limit: 25,
  sortBy: 'created_at',
  sortOrder: 'desc',
  filters: [
    {
      domain: 'go.example.com',
      tracking: AnalyticsMode.Full,
      safeMode: true,
      isPrivate: false,
      password: false,
      status: LinkStatus.Active
    }
  ]
});

const updated = await fyn.links.update({
  id: 'link_123',
  target: 'https://example.com/updated',
  title: 'Updated title',
  notes: 'Updated notes',
  tags: ['updated'],
  iosTarget: 'https://ios.example.com/updated',
  androidTarget: 'https://android.example.com/updated',
  geoTargets: {
    US: 'https://us.example.com/updated'
  },
  password: null,
  safeMode: true,
  privateLink: false,
  analytics: AnalyticsMode.Clicks,
  expirySeconds: null
});

await fyn.links.delete('link_123');

Possible SDK response (fyn.links.get):

{
  id: 'link_123',
  userId: 'user_99',
  teamId: 'team_7',
  domain: 'go.example.com',
  slug: 'launch-2026',
  shortUrl: 'https://go.example.com/launch-2026',
  target: {
    default: 'https://example.com',
    ios: null,
    android: null,
    geo: {}
  },
  title: 'Launch Page',
  notes: 'Primary campaign link',
  tags: ['launch', 'campaign'],
  isPrivate: false,
  safeMode: true,
  viaApi: true,
  passwordEnabled: false,
  clicks: 42,
  createdAt: '2026-03-20T10:15:30.000000Z',
  updatedAt: '2026-03-21T09:00:00.000000Z',
  expiresAt: null,
  tracking: 'full',
  status: 'active'
}

Possible SDK response (fyn.links.list):

{
  data: [
    {
      id: 'link_123',
      userId: 'user_99',
      teamId: 'team_7',
      domain: 'go.example.com',
      slug: 'launch-2026',
      shortUrl: 'https://go.example.com/launch-2026',
      target: {
        default: 'https://example.com',
        ios: null,
        android: null,
        geo: {}
      },
      title: 'Launch Page',
      notes: 'Primary campaign link',
      tags: ['launch', 'campaign'],
      isPrivate: false,
      safeMode: true,
      viaApi: true,
      passwordEnabled: false,
      clicks: 42,
      createdAt: '2026-03-20T10:15:30.000000Z',
      updatedAt: '2026-03-21T09:00:00.000000Z',
      expiresAt: null,
      tracking: 'full',
      status: 'active'
    }
  ],
  meta: {
    current_page: 1,
    from: 1,
    to: 1,
    per_page: 25,
    last_page: 1,
    total: 1
  }
}

Possible SDK response (fyn.links.update):

{
  id: 'link_123',
  userId: 'user_99',
  teamId: 'team_7',
  domain: 'go.example.com',
  slug: 'launch-2026',
  shortUrl: 'https://go.example.com/launch-2026',
  target: {
    default: 'https://example.com/updated',
    ios: 'https://ios.example.com/updated',
    android: 'https://android.example.com/updated',
    geo: {
      US: 'https://us.example.com/updated'
    }
  },
  title: 'Updated title',
  notes: 'Updated notes',
  tags: ['updated'],
  isPrivate: false,
  safeMode: true,
  viaApi: true,
  passwordEnabled: false,
  clicks: 42,
  createdAt: '2026-03-20T10:15:30.000000Z',
  updatedAt: '2026-03-21T10:22:00.000000Z',
  expiresAt: null,
  tracking: 'clicks',
  status: 'active'
}

fyn.links.delete(id) resolves with no return value when successful.

fyn.links.list(input) parameters:

| Field | Type | Description | | --- | --- | --- | | page | number | Page number. | | limit | number | Page size. | | sortBy | 'created_at' \| 'updated_at' \| 'clicks' | Sort field. | | sortOrder | 'asc' \| 'desc' | Sort direction. | | filters | LinkListFilter[] | Filter rules applied by the API. |

LinkListFilter fields:

| Field | Type | Description | | --- | --- | --- | | domain | string | Filter by domain. | | tracking | AnalyticsMode | Filter by analytics mode. | | safeMode | boolean | Filter by safe mode setting. | | isPrivate | boolean | Filter by privacy setting. | | password | boolean | Filter by password protection status. | | status | LinkStatus | Filter by link status. |

viaApi is response metadata on LinkRecord and is set by the backend. It is not a request/filter input.

Available link statuses:

  • LinkStatus.Active
  • LinkStatus.Inactive
  • LinkStatus.Flagged
  • LinkStatus.Reported
  • LinkStatus.ReportedBanned
  • LinkStatus.NeedsUpgrade
  • LinkStatus.Quarantined

fyn.links.update(input) parameters:

| Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | Yes | Link ID to update. | | target | string | No | New default destination URL. | | title | string | No | Updated title. | | notes | string | No | Updated notes. | | tags | string[] | No | Updated tags. | | iosTarget | string | No | Updated iOS destination URL. | | androidTarget | string | No | Updated Android destination URL. | | geoTargets | Record<string, string> | No | Updated country-specific targets. | | password | string \| null | No | New password, or null to remove it. | | safeMode | boolean | No | Updated safe mode setting. | | privateLink | boolean | No | Updated privacy setting. | | analytics | AnalyticsMode | No | Updated analytics mode. | | expirySeconds | number \| null | No | Updated expiry, or null to clear it. |

Analytics

Available methods:

  • fyn.analytics.clicks(linkId)
  • fyn.analytics.timeseries(linkId, query?)
  • fyn.analytics.browsers(linkId, query?)
  • fyn.analytics.devices(linkId, query?)
  • fyn.analytics.countries(linkId, query?)
  • fyn.analytics.referrers(linkId, query?)

MetricsQuery parameters:

| Field | Type | Description | | --- | --- | --- | | start | string | Start timestamp. | | end | string | End timestamp. | | granularity | 'minute' \| 'hour' \| 'day' \| 'week' \| 'month' | Aggregation granularity. | | country | string[] | Country filters. | | browser | string[] | Browser filters. | | device | string[] | Device filters. | | referrer | string[] | Referrer filters. |

Example:

const stats = await fyn.analytics.timeseries('link_123', {
  start: '2026-03-01T00:00:00.000Z',
  end: '2026-03-19T23:59:59.999Z',
  granularity: 'day',
  country: ['US', 'IN'],
  browser: ['Chrome', 'Safari'],
  device: ['desktop', 'mobile'],
  referrer: ['google.com']
});

Possible SDK responses:

// fyn.analytics.clicks('link_123')
{
  id: 'link_123',
  clicks: 120
}

// fyn.analytics.timeseries('link_123', query)
{
  data: [
    {
      date: '2026-03-01T00:00:00+00:00',
      clicks: 14,
      uniqueClicks: 12
    }
  ],
  meta: {
    totalClicks: 120,
    totalUniqueClicks: 95,
    count: 10
  }
}

// fyn.analytics.browsers('link_123', query)
{
  data: [
    { browser: 'Chrome', clicks: 58, percentage: 48.33 },
    { browser: 'Safari', clicks: 29, percentage: 24.17 }
  ],
  meta: {
    totalClicks: 120,
    count: 2
  }
}

// fyn.analytics.devices('link_123', query)
{
  data: [
    { device: 'desktop', clicks: 73, percentage: 60.83 },
    { device: 'mobile', clicks: 47, percentage: 39.17 }
  ],
  meta: {
    totalClicks: 120,
    count: 2
  }
}

// fyn.analytics.countries('link_123', query)
{
  data: [
    { country: 'United States', countryCode: 'US', clicks: 64, percentage: 53.33 },
    { country: 'India', countryCode: 'IN', clicks: 28, percentage: 23.33 }
  ],
  meta: {
    totalClicks: 120,
    count: 2,
    countriesCount: 2
  }
}

// fyn.analytics.referrers('link_123', query)
{
  data: [
    { referrer: 'google.com', clicks: 66, percentage: 55.0 },
    { referrer: 'direct', clicks: 22, percentage: 18.33 }
  ],
  meta: {
    totalClicks: 120,
    count: 2,
    sourcesCount: 2
  }
}

Domains

const domains = await fyn.domains.list({
  page: 1,
  limit: 10
});

const domain = await fyn.domains.get('domain_123');

const created = await fyn.domains.create({
  domain: 'go.example.com',
  description: 'Primary branded short domain',
  notFoundUrl: 'https://example.com/404',
  rootRedirect: 'https://example.com',
  gpc: false
});

const verification = await fyn.domains.verify(created.id);

const updated = await fyn.domains.update({
  id: created.id,
  description: 'Updated description',
  notFoundUrl: 'https://example.com/not-found',
  rootRedirect: 'https://example.com/home',
  gpc: true
});

await fyn.domains.remove(created.id);

Possible SDK responses:

// fyn.domains.list(...)
{
  data: [
    {
      id: 'domain_123',
      domain: 'go.example.com',
      description: 'Primary branded short domain',
      isSubdomain: true,
      userId: 'user_99',
      teams: ['team_7'],
      status: 'pending',
      notFoundUrl: 'https://example.com/404',
      rootRedirect: 'https://example.com',
      gpc: false,
      createdAt: '2026-03-20T09:00:00.000000Z',
      updatedAt: '2026-03-20T09:00:00.000000Z'
    }
  ],
  meta: {
    current_page: 1,
    from: 1,
    to: 1,
    per_page: 10,
    last_page: 1,
    total: 1
  }
}

// fyn.domains.get('domain_123')
{
  id: 'domain_123',
  domain: 'go.example.com',
  description: 'Primary branded short domain',
  isSubdomain: true,
  userId: 'user_99',
  teams: ['team_7'],
  status: 'pending',
  notFoundUrl: 'https://example.com/404',
  rootRedirect: 'https://example.com',
  gpc: false,
  createdAt: '2026-03-20T09:00:00.000000Z',
  updatedAt: '2026-03-20T09:00:00.000000Z'
}

// fyn.domains.create(...)
{
  id: 'domain_123',
  domain: 'go.example.com',
  description: 'Primary branded short domain',
  isSubdomain: true,
  userId: 'user_99',
  teams: ['team_7'],
  status: 'pending',
  notFoundUrl: 'https://example.com/404',
  rootRedirect: 'https://example.com',
  gpc: false,
  createdAt: '2026-03-20T09:00:00.000000Z',
  updatedAt: '2026-03-20T09:00:00.000000Z'
}

// fyn.domains.verify('domain_123')
{
  id: 'domain_123',
  domain: 'go.example.com',
  status: 'active',
  verified: true,
  cname: 'cname.fyn.link'
}

// fyn.domains.update(...)
{
  id: 'domain_123',
  domain: 'go.example.com',
  description: 'Updated description',
  isSubdomain: true,
  userId: 'user_99',
  teams: ['team_7'],
  status: 'active',
  notFoundUrl: 'https://example.com/not-found',
  rootRedirect: 'https://example.com/home',
  gpc: true,
  createdAt: '2026-03-20T09:00:00.000000Z',
  updatedAt: '2026-03-21T08:30:00.000000Z'
}

fyn.domains.remove(id) resolves with no return value when successful.

fyn.domains.list(input) parameters:

| Field | Type | Description | | --- | --- | --- | | page | number | Page number. | | limit | number | Page size. |

fyn.domains.create(input) parameters:

| Field | Type | Required | Description | | --- | --- | --- | --- | | domain | string | Yes | Domain name to register. | | description | string | No | Optional description. | | notFoundUrl | string | No | URL used for unmatched slugs. | | rootRedirect | string | No | URL used for the bare domain root. | | gpc | boolean | No | Global privacy control setting. |

When you create a domain, the backend automatically associates it with the current team from the token used for the request. You do not pass a team in fyn.domains.create().

fyn.domains.update(input) parameters:

| Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | Yes | Domain ID to update. | | description | string | No | Updated description. | | notFoundUrl | string | No | Updated not found URL. | | rootRedirect | string | No | Updated root redirect URL. | | gpc | boolean | No | Updated global privacy control setting. |

Domain team ownership is not changed through fyn.domains.update().

Members

const members = await fyn.members.list({
  includeOwner: true
});

const member = await fyn.members.get('member_123');

await fyn.members.invite({
  email: '[email protected]',
  role: 'editor'
});

const updated = await fyn.members.update({
  id: 'member_123',
  role: 'admin'
});

await fyn.members.remove('member_123');

Possible SDK responses:

// fyn.members.list(...)
{
  data: [
    {
      id: 'user_123',
      name: 'Alex Doe',
      email: '[email protected]',
      role: 'editor',
      displayPicture: 'https://cdn.example.com/avatar/alex.png',
      teamId: 'team_7',
      createdAt: '2026-03-01T11:00:00.000000Z',
      updatedAt: '2026-03-15T09:00:00.000000Z'
    }
  ],
  meta: {
    current_page: 1,
    from: 1,
    to: 1,
    per_page: 15,
    last_page: 1,
    total: 1
  }
}

// fyn.members.get('member_123')
{
  id: 'user_123',
  name: 'Alex Doe',
  email: '[email protected]',
  role: 'editor',
  displayPicture: 'https://cdn.example.com/avatar/alex.png',
  teamId: 'team_7',
  createdAt: '2026-03-01T11:00:00.000000Z',
  updatedAt: '2026-03-15T09:00:00.000000Z'
}

// fyn.members.update(...)
{
  id: 'user_123',
  name: 'Alex Doe',
  email: '[email protected]',
  role: 'admin',
  displayPicture: 'https://cdn.example.com/avatar/alex.png',
  teamId: 'team_7',
  createdAt: '2026-03-01T11:00:00.000000Z',
  updatedAt: '2026-03-21T10:00:00.000000Z'
}

fyn.members.invite(...) and fyn.members.remove(...) both resolve with no return value on success.

fyn.members.list(input) parameters:

| Field | Type | Description | | --- | --- | --- | | includeOwner | boolean | Includes the team owner in the result set. |

fyn.members.invite(input) parameters:

| Field | Type | Required | Description | | --- | --- | --- | --- | | email | string | Yes | Member email address. | | role | string | Yes | Member role. |

For security, only already-registered users can be invited.

fyn.members.update(input) parameters:

| Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | Yes | Member ID to update. | | role | string | Yes | Updated role. |

Teams

const team = await fyn.teams.get();

const renamed = await fyn.teams.update({
  name: 'Growth Team'
});

Possible SDK response (fyn.teams.get and fyn.teams.update):

{
  id: 'team_7',
  type: 'team',
  attributes: {
    name: 'Growth Team',
    privateLinks: false,
    safeMode: true,
    tracking: 'full'
  },
  owner: {
    id: 'user_99',
    type: 'user',
    name: 'Owner Name'
  },
  defaultDomain: {
    id: 'domain_123',
    value: 'go.example.com'
  },
  createdAt: '2026-01-01T00:00:00+00:00',
  updatedAt: '2026-03-21T10:15:00+00:00'
}

fyn.teams.update(input) parameters:

| Field | Type | Required | Description | | --- | --- | --- | --- | | name | string | Yes | New team name. |

Info

const info = await fyn.info.get();
const freshInfo = await fyn.info.get(true);

Use fyn.info.get() to fetch bootstrap information for the current token and team. Pass true to force a refresh.

Possible SDK response:

{
  id: 'token_abc',
  user: {
    id: 'user_99',
    name: 'Owner Name'
  },
  team: {
    id: 'team_7',
    name: 'Growth Team',
    publicKey: 'base64-public-key',
    disableAnalytics: false,
    safeMode: true,
    privateLink: false,
    defaultDomain: {
      id: 'domain_123',
      value: 'go.example.com'
    },
    availableDomains: [
      { id: 'domain_123', value: 'go.example.com' },
      { id: 'domain_124', value: 'links.example.com' }
    ]
  },
  permissions: ['link.read', 'link.create', 'member.read'],
  tokenName: 'SDK Token'
}

Caching

Caching is enabled by default with ETag revalidation support for GET requests.

const fyn = new Fyn({
  token: process.env.FYN_API_TOKEN!,
  secret: process.env.FYN_SECRET_KEY,
  cache: {
    enabled: true,
    ttlMs: 60_000,
    storage: 'disk',
    path: '.fynlink-sdk-cache.json',
    warnOnDisk: true
  }
});

Security notes:

  • Disk cache stores raw GET response bodies and headers.
  • Link-sensitive fields (for example target, password, notes, tags, short URL envelope) remain encrypted inside cached payload envelopes.
  • Metadata and headers are cached as returned; they are not encrypted by the SDK cache layer.
  • If an attacker obtains both cache contents and your FYN_SECRET_KEY, encrypted link payload fields may be decrypted.
  • Keep cache.warnOnDisk enabled and use disk cache only in trusted environments.

Error Handling

The SDK throws typed errors:

  • FynAuthError
  • FynPermissionError
  • FynNotFoundError
  • FynValidationError
  • FynRateLimitError
  • FynTimeoutError
  • FynError
import { FynValidationError } from '@fynlink/sdk';

try {
  await fyn.links.create({
    domain: 'go.example.com',
    target: 'https://example.com'
  });
} catch (error) {
  if (error instanceof FynValidationError) {
    console.error(error.message);
  }
}

Local Examples

cp examples/.env.example examples/.env

Example environment variables:

FYN_API_TOKEN=...
FYN_SECRET_KEY=...
FYN_API_URL=https://api.fyn.link/v1
FYN_DEBUG=false
FYN_CACHE_ENABLED=true
FYN_CACHE_TTL_MS=300000
FYN_CACHE_STORAGE=disk
FYN_CACHE_PATH=.fynlink-sdk-cache.json
FYN_CACHE_WARN_ON_DISK=true

Available scripts:

npx tsx examples/links/create.ts
npx tsx examples/links/get.ts <linkId>
npx tsx examples/links/list.ts
npx tsx examples/links/update.ts <linkId>
npx tsx examples/links/delete.ts <linkId>
npx tsx examples/info/get.ts
npx tsx examples/analytics/clicks.ts <linkId>
npx tsx examples/analytics/timeseries.ts <linkId>
npx tsx examples/analytics/browsers.ts <linkId>
npx tsx examples/analytics/devices.ts <linkId>
npx tsx examples/analytics/countries.ts <linkId>
npx tsx examples/analytics/referrers.ts <linkId>
npx tsx examples/members/list.ts
npx tsx examples/members/get.ts <memberId>
npx tsx examples/members/invite.ts <email> <role>
npx tsx examples/members/update.ts <memberId> <role>
npx tsx examples/members/remove.ts <memberId>
npx tsx examples/teams/get.ts
npx tsx examples/teams/update.ts <new-team-name>
npx tsx examples/domains/list.ts
npx tsx examples/domains/get.ts <domainId>
npx tsx examples/domains/create.ts <domain>
npx tsx examples/domains/update.ts <domainId>
npx tsx examples/domains/verify.ts <domainId>
npx tsx examples/domains/delete.ts <domainId>

More examples: examples/README.md

Development

npm install
npm run typecheck
npm test
npm run build

License

MIT