Skip to content

Auth and Tokens

When your media sources require authorization: HLS streams behind a CDN auth wall, APIs that return 401 when tokens expire, or endpoints that enforce access control via 403.

Prerequisites: The auth config is the same on video and music players. For the full reference see Core: Auth and Fetch.

Minimal auth setup

TypeScript
import { nmplayer } from '@nomercy-entertainment/nomercy-video-player';

const player = nmplayer('main').setup({
playlist: [{ id: '1', url: 'https://protected.cdn.your-domain.com/stream.m3u8' }],
auth: {
// Called on every authenticated request. Use a factory so expired tokens are always fresh:
bearerToken: () => myAuth.getAccessToken(),

// Called once when a request returns 401. Refresh the token here; the player
// re-reads bearerToken() automatically and retries the failed request once.
refreshOnUnauthenticated: async () => {
await myAuth.refresh();
},
},
});

The player injects Authorization: Bearer <token> on every HLS manifest and segment request.

401 vs 403: a hard rule

StatusWhat happens
401refreshOnUnauthenticated called once → bearerToken re-read → request retried → if refresh fails, the request rejects with core:auth/refresh-failed on the error channel
403Error propagates immediately. refreshOnUnauthenticated is never called.

Do not lump 401 and 403 in the same catch block. A 403 is an entitlement decision, so retrying with a refreshed token achieves nothing. The right response to a 403 is to check the user’s subscription or permissions on your server.

TypeScript
// Listen for auth events:
player.on('auth:refreshed', ({ tokenAcquiredAt }) => {
// Token was refreshed successfully. tokenAcquiredAt is a Date.now() timestamp.
console.log('Token refreshed at', tokenAcquiredAt);
});

player.on('auth:failed', ({ error }) => {
// Refresh failed, redirect to login
router.push('/login');
});

Static vs factory token

TypeScript
// Static string. Use only when the token does not expire (rare):
auth: {
bearerToken: 'eyJhbGciOi...',
}

// Factory (recommended). Called fresh on every request:
auth: {
bearerToken: () => myAuth.getAccessToken(),
}

// Async factory. Use when your token retrieval is async:
auth: {
bearerToken: async () => {
const token = await myAuth.getAccessToken();

return token;
},
}

Use the factory form whenever the token can expire. A stale static string will cause 401s until the page is refreshed.

Updating auth at runtime

After setup(), replace the auth config without recreating the player:

TypeScript
// Merge-update. Only the provided fields are updated:
player.auth({
bearerToken: newToken,
});

// Replace entirely:
player.auth({
bearerToken: () => newAuth.getAccessToken(),
refreshOnUnauthenticated: async () => {
await newAuth.refresh();
return newAuth.getAccessToken();
},
});

Useful when a user signs in during a session or when a long-lived token is refreshed by an external mechanism.

Static extra headers

Add headers to every authenticated request (e.g. app version, device ID):

TypeScript
auth: {
bearerToken: () => myAuth.getAccessToken(),
headers: {
'X-App-Version': '2.0.0',
'X-Device-Id': deviceId,
},
},

Custom request signing (HMAC, signed URLs)

For non-Bearer auth schemes, use signRequest:

TypeScript
auth: {
signRequest: async (request: Request): Promise<Request> => {
// Clone the request to make it mutable:
const signed = request.clone();
const signature = await myHmac.sign(await request.text());
return new Request(request, {
headers: {
...Object.fromEntries(request.headers.entries()),
'X-Signature': signature,
},
});
},
},

signRequest is called after bearerToken is applied. Both can be used together.

Using authFetch inside plugins

Inside a plugin, always use this.fetch instead of the global fetch. This applies the player’s auth config, respects the retry policy, and integrates with error reporting:

TypeScript
import { Plugin } from '@nomercy-entertainment/nomercy-player-core';
import type { NMVideoPlayer } from '@nomercy-entertainment/nomercy-video-player';

class MetadataPlugin extends Plugin<NMVideoPlayer, { endpoint: string }, {}> {
static readonly id = 'myapp:metadata';
static readonly version = '1.0.0';
static readonly description = 'Fetches item metadata from the NoMercy server';

use(): void {
this.on('current', async ({ item }) => {
if (!item) return;

// this.fetch automatically injects the auth header:
const meta = await this.fetch<{ synopsis: string }>(
`${this.opts.endpoint}/${item.id}`,
{ responseType: 'json' },
);

// Use the metadata...
console.log(meta.synopsis);
});
}

dispose(): void {}
}

Retry policy

By default only specific error codes retry (DEFAULT_RETRY_POLICY): 5xx and timeouts a few times with backoff, fragment loads up to 5; generic 4xx and unmatched failures use attempts: 0 (no retry). Auth refresh is separate — refreshOnUnauthenticated is called exactly once, never retried.

There is no setup-level retry option. To override retry behaviour for a specific request, pass a RetryConfig as the retry option on this.fetch() inside a plugin:

TypeScript
// inside a plugin's use()
await this.fetch(url, {
retry: { attempts: 5, backoff: 'exponential', baseMs: 200, maxMs: 10_000 },
});