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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@amedia/user

v1.0.5

Published

Client lib for working with aID user and associated data

Readme

@amedia/user

The goal of this module is to simplify the usage of aID data and services in Amedia frontend code. The module is designed to be used several places on a webpage independently, and the module will ensure that requests from different parts of the webpage are handled efficiently.

TL;DR (details in the linked chapters)

  • UserDataRequest User data like name, access, KV-storage, etc. Call it as much as you like, but only ask for what you actually need.
  • PaywallUnlockRequest When you need to actively verify login state and access. May perform redirects and expensive calls for subscription activation, and thus limited in how much it accepts to be called.
  • SiteAccessRequest When you need to actively verify access. Specify which access feature(s) you need. May attempt subscription activation.
  • User actions Login, logout, that sort of thing.

Usage

Please be advised that you are required to use ESM to use this module.

Importing the module

Add this module to your project by running npm install -d @amedia/user. This will allow for type-hinting. @amedia/user requires a browser to run. If your app performs server-side rendering, consult framework documentation on how to do this.

Getting user data

To get user data, you first need to create a new UserDataRequest() instance, then specify what data you need and lastly decide if you want to wait until all data is available, or you want to get partial updates.

Data model

Attributes

| name | type | description | | ------------------ | ------------- | ------------------------------------------------------- | | uuid | string | The users unique ID. (NOT for tracking). | | trackingKey | string | Unique ID of user that can be used for tracking. | | sessionTrackingKey | string | Tracking key for the user session. | | name | string | Users full name. | | access | Array | Users access on current site (list of access features). |

Attributes are requested by calling .withAttributes(attributes: Array<string>) on a UserDataRequest with a list of the attributes you need.

In the response, attributes is an object where each requested attribute is an attribute. So you can get the name by accessing attributes.name for example.

NOTE: Don't request attributes you don't intend to use. This previously had a latency-cost attached to it. This is no longer the case. However, it's best not to keep user data around unnecessarily.

Storage

Storage contains namespaces stored in aID (Europa). A namespace is a named set of key value pairs stored for a user. An example could be favourite_teams namespace, which could contain team_id as a key and timestamp it was stored as a value.

Storage is requested by calling .withStorage(namespaces: Array<string>) on a UserDataRequest. If a namespace does not exist, it will be "created" in that you'll get an empty object back. Saving data to it will perform the actual creation.

In the response, storage is an object with each namespace as an attribute which leads to an object with attributes for each key in the namespace. So you can access storage.favourite_teams.myteamid and get the value stored for teamid in the favourite_teams namespace. This value will be whatever was saved there, but always a string.

type Storage = { [namespace: string]: Record<string, string> };

Client Storage

This works just as regular storage, but the namespaces are scoped to oauth clients.

Client Storage is requested by calling .withClientStorage(clientId: string, namespaces: Array<string>) on a UserDataRequest.

In the response, clientStorage[clientId] is an object with each namespace as en attribute that works as described above.

type ClientStorage = { [clientId: string]: Storage };

Writing to storage namespaces

A namespace is a proxied instance of Namespace. You access this as any old object, but it provides a save()-method for when you wish to persist this namespace for later use.

import { UserDataRequest } from '@amedia/user';

const { storage } = await new UserDataRequest()
  .withStorage(['my_namespace'])
  .fetch();
storage.my_namespace.my_key = 'my data';
storage.my_namespace.save();

delete storage.my_namespace.my_key;
storage.my_namespace.save();

In this example, my_key in my_namespace would be updated to my data (and then deleted). Other data would probably be left "untouched." Meaning, if this namespace sees a lot of changes outside this instance, you may end up overwriting other changes if you delay a lot between reading and writing!

State

State is meant to describe the current context the user is in right now. It's not directly requested but will always be a part of the response.

Current attributes in state:

| name | type | description | | ------------- | ------------- | ----------------------------------------------------------------------------- | | isLoggedIn | boolean | Is the user currently logged in? If not, most attributes will be undefined | | emergencyMode | Array | Current state of emergency mode in production environment (paywall, aid etc.) |

Access not directly attached to a user

A visitor may have access through their IP-address, or a specific token that we periodically enable for "avis i skolen".

Current elements in the array nonUserAccess:

| name | type | descriptions | | -------------- | ------------- | -------------------- | | customer | string | "conocophillips" etc | | accessFeatures | Array | ["pluss"] |

Executing the request

Once you have created a request and specified the data you need, the final step is to decide how you want to get the data once it's available. You can either choose to subscribe to data as they get loaded from the various backends or wait until all fetching is done and get everything in one package.

The response to the request will be an object with attributes for state, attributes and storage. In the examples below these are unpacked for easier access.

Subscribing to updates

The most flexible way to get userdata is by subscribing to updates. This is done by calling .subscribe() on the UserDataRequest with a callback function that can handle the updates once they arrive. This is especially useful when the time it can take to get the data can vary. Getting the user's name is quick, so you can display the user's name before you have received storage, for example.

Full usage example:

import { UserDataRequest } from '@amedia/user';

const unsubscribe = new UserDataRequest()
  .withAttributes(['name', 'trackingKey'])
  .withStorage(['my_namespace'])
  .withClientStorage('client-id', ['my-client-namespace'])
  .withNonUserAccess()
  .subscribe(({ attributes, state, storage, clientStorage, nonUserAccess }) => {
    console.log(attributes, state, storage, clientStorage, nonUserAccess);
  });

// If you wish to unsubscribe from further updates:
unsubscribe();

The callback you supplied to subscribe is called whenever there are any changes to the properties you have requested, so be prepared to re-render.

Also, your requested properties might not arrive all at once. This callback will be called whenever we get data in from the various services.

You should check the state object for potential emergency modes and act accordingly.

Should you wish to end your subscription, just call the returned function from subscribe.

Fetching all the data at once (no re-renders)

In many use-cases, it's more convenient to just wait until everything is ready, so you don't have to handle multiple callbacks. In these cases, it's easier to call .fetch(). This method will return a Promise that resolves the same object you would get in the subscribe callback.

Full usage example:

import { UserDataRequest } from '@amedia/user';

new UserDataRequest()
  .withContext('my-app') // Tell us who you are :) This helps with tracability.
  .withAttributes(['name'])
  .withStorage(['my_namespace'])
  .withClientStorage('client-id', ['my-client-namespace'])
  .withNonUserAccess()
  .fetch()
  .then(({ attributes, state, storage, clientStorage, nonUserAccess }) => {
    console.log(attributes, state, storage, clientStorage, nonUserAccess);
  });

Fetch will wait until it has all the data you requested before resolving, unless the user is not currently logged in. Internally .fetch() will use .subscribe() and just wait until everything is ready before unsubscribing and then resolving the returned promise.

If you really need to render the user's name or get their uuid etc. as fast as possible, use .subscribe(), if not, this is probably easier.

Timeouts when using fetch

Especially poor connections may result in almost eternal wait times - which is no good if your app is locked while waiting on the data. Because of this we have introduced a timeout. A timeout will cause the fetch to reject with FetchTimeoutError.

FetchTimeoutError will include any data we might have gotten before we timed out

new UserDataRequest()
  .withContext('my-app')
  .withAttributes(['name'])
  .withStorage(['my_namespace'])
  .fetch()
  .catch((e) => {
    if (e instanceof FetchTimeoutError) {
      console.log(e.partialData);
      // Maybe you're able to render your app in a reduced state (using partialData).
    }
  });

If you're running a background task and don't mind waiting longer than default (10 seconds), you may set a longer timeout in fetch:

new UserDataRequest()
  .withContext('my-app')
  .withAttributes(['name'])
  .withStorage(['my_namespace'])
  .fetch({ timeout: 60000 /* milliseconds */ });

This may also be lowered below the default, but network request do take some time, even on fast networks, so don't shoot your app in the foot by lowering it too much.

Emergency mode when using fetch

To avoid hanging on timeout, fetch rejects with EmergencyModeError, when a relevant emergency mode is active. If you receive this error, there will be no user data available. Your app should act accordingly, eg:

  • Render non-personalized
  • Show a placeholder with an error message
  • Not rendering at all
new UserDataRequest()
  .withContext('my-app')
  .withAttributes(['name'])
  .withStorage(['my_namespace'])
  .fetch()
  .catch((e) => {
    if (e instanceof FetchTimeoutError) {
      console.log(e.partialData);
      // Maybe you're able to render your app in a reduced state (using partialData).
    } else if (e instanceof EmergencyModeError) {
      console.log(e.activeEmergencyModes);
      // Maybe you're able to render your app in a non-personalized way?
    }
  });

Actions

getLoginUrl({requestedUrl: string context: string}): string

Builds and return the aID login URL. See goToLoginPage for details

goToLoginPage({requestedUrl: string context: string}): void

Send the user to the aID login page. It will take care of building the correct URL, including your current site domain, so the login page is skinned correctly. By default, the user will be sent back to the current url after login has been completed. You may include a requestedUrl if you need to control where the user should end up.

Note: We also support sending a context string when requesting a login. Please include your app-name or more recognizable string. If your app has multiple places for requesting login, it's nice to include that info as well. Eg: goToLoginPage({context: my-app_switch-user}). The context will help us understand where logins originate from to assist in debugging.

requestDataRefresh(): void

If you need to ensure the latest data is available, you may request a data refresh. @amedia/user will look up all data it has in the cache and contact various endpoints to ensure they are up to date. If you're subscribing to updates, your callback will be executed if anything changes.

Note: A data refresh will be automatically performed every time the page regains focus (eg. user switches tabs)

logout(): Promise<void>

This action will log the user out of aID. The logout is global, meaning it will log the session out of all sites and aid.no. After logout has completed the promise will resolve, giving your app the opportunity to reset state / reload the page etc.

pollForAccess(requredAccessFeatures = ['pluss'], timeout_millis = 3000): Promise<true>

Polling for expected access. Will either return true when required access features are attained, or TimeoutError.

aidUrls: Record<string, URL>

Get various, environment-aware URLs to pages often linked to from other apps. This includes pages like the aID profile page, Terms of Service, Privacy Statement, Privacy Preferences, Family Sharing and the like.

Unlocking the paywall

Sites hosted through Amedia may use a Paywall. This is based on a JWT-cookie called aid.jwt that is validated and parsed by Varnish. When content is requested from a backend, it returns with header information used by varnish to decide if the content is available to the reader, and a header telling varnish where to get the version that includes the paywall. The paywall (incentive) will use PaywallUnlockRequest to see if it can somehow grant access to the user.

In some cases it makes sense to create a different user interface for a paywall. If so we recommend that you use PaywallUnlockRequest in order to get the same functionality.

More information about the paywall can be found here: How the Paywall works

Information about the incentive can also be useful, since you probably need to replicate this functionality if you create a new user interface: How the Incentive works

To unlock the paywall, you first need to create a new PaywallUnlockRequest. Once created, you can set some options by calling methods on this request:

  • reloadOnAccess(): If the paywall unlocker finds sufficient access for the user, it will reload the current page
  • redirectOnAccess(requestedUrl: string): If the paywall unlocker finds sufficient access for the user, it will send the user to requestedUrl. (Provides more flexibility than reloadOnAccess())
  • Deprecated (pass in constructor instead): withAccessFeatures(accessFeatures: string[]): Specify what access features will give access to the current content.

More details about the access model can be found here: Access model

Once your options have been set up, you can call tryUnlock(). This is when all the magic happens. The unlocker will try to autologin the user on the current domain, check access, activate subscriptions in the subscription system, check for stuff like IP-based access, etc. When this is done, it will resolve the Promise that was returned by tryUnlock().

The resolution to the promise contains an object with attributes you can use to get details about why the user did not get access (or if he had access if you don't opt for reload or redirect). The object contains the following:

  • isLoggedIn: True if the user is logged in, false otherwise. Useful if you want to show a login button, which makes no sense for logged-in users.
  • hasAccess: True if the user has access. Always false if you called reloadOnAccess() or redirectOnAccess().
  • accessFeatures: List of access features the user has access to. Empty list if noy logged in or has no access. This list is useful if you want to upgrade from 'pluss' to 'plussalt' for instance. You might want to give a different user experience to users with some access than users with no access.

Full usage example:

import { PaywallUnlockRequest } from '...';

new PaywallUnlockRequest(['pluss'])
  .redirectOnAccess('https://my.neat/place') // Optional
  .reloadOnAccess() // Optional
  .tryUnlock()
  .then(({ isLoggedIn, hasAccess, accessFeatures }) => {
    // If you did not specify any "on access"-action above
    // we will return here afterwards.
    console.log({ isLoggedIn, hasAccess, accessFeatures });
  })
  .catch((error) => {
    // Known potential errors are:

    // This guards potential reload/redirect loop. Access should have been granted at this time
    PaywallError(
      'Unlock check already performed.',
      PaywallError.ERROR_RELOADED_NO_ACCESS
    );

    // Internal error. Access should have been granted at this time.
    PaywallError(
      'New subscription was attached and checked successfully, but internal systems disagree about current site access.',
      PaywallError.ERROR_NO_ACCESS_AFTER_SUCCESSFUL_ACTIVATION_WITH_REQUESTED_ACCESS_FEATURES
    );

    // Internal error. We are unable to get access tokens/cookies to set.
    PaywallError(
      'Could not set access cookies',
      PaywallError.ERROR_ENABLING_ACCESS_FAILED
    );
  });

Note that this might result in redirects/reloads if the user is not already logged in. In that case you will end up here again if logging in did not result in access.

Activating subscriptions / finding upgrade path

SiteAccessRequest checks the current users' access to a specific site. If you're on a site that has its own access features (e.g. has a paywall), you should use PaywallUnlockRequest instead. This will set cookies as well.

  • withSiteDomain(siteDomain: string) The domain you with to check access for / attempt activation against.
  • withAccessFeatures(accessFeatures: Array<string>) Which access features do you require in order to consider access is granted.

Once your options have been set up, you can call requestAccess(). This is when all the magic happens. It will try to autologin the user on the current domain, check access, activate subscriptions in the subscription systems, check for stuff like IP-based access etc. When this is done, it will resolve the Promise that was returned.

The resolved promise contains the following:

  • isLoggedIn: True if the user is logged in, false otherwise. Useful if you want to show a login button (see goToLoginPage(...)).
  • hasAccess: True if the user has access that fulfils your withAccessFeatures-requirements.
  • accessFeatures: List of access features the user actually has at this time. Empty list if user is logged out or has no access. This list is useful if you want to upgrade from 'newspaper' to 'plussalt' for instance. You might want to give a different user experience to users with some access than users with no access.
  • primarySite: Best guess of where the user may attain the access you requested.

Full usage example:

import { SiteAccessRequest } from '@amedia/user';
import { SiteAccessResolverError } from './SiteAccessRequest';

new SiteAccessRequest(['sport_premium'])
  .withSiteDomain('www.direktesport.no')
  .requestAccess()
  .then(({ isLoggedIn, hasAccess, primarySite, accessFeatures }) => {
    console.log({ isLoggedIn, hasAccess, primarySite, accessFeatures });
  })
  .catch((error) => {
    // Known potential errors are:

    // You would have to show some error / try again mechanic etc here.
    SiteAccessRequestError(
      'Unable to resolve access for site (www.direktesport.no)'
    );

    // If you attempt this on localhost
    Error(
      `@amedia/user cannot request site access for (www.direktesport.no) on localhost`
    );
  });

Note that this might result in redirects/reloads if the user is not already logged in. In that case you will end up here again if logging in did not result in access.

Known limitations

Subscription activation can only happen against the subscription system related to the withSiteDomain. #team-abo will need to look into activation across all subscription systems to improve this behavior.

Performance

This app aims to be able to deliver a lot of data fast. Still, we might need to call several services to get what you need. We do try to cache whenever this makes sense, but please don't ask for data you don't need.

Behind the scenes

There are a few techniques used to avoid over-fetching and fetching multiple times when once would do.

Event-based actions

Much like node-userdata-browser/userdata-client did, we use events to message that data is being requested. This allowed us to decouple your request from the particular instance that will serve it. That in turn allows us to have this library loaded with multiple different components across the page and only process the requests in one place. In addition, if the component currently processing requests should be unloaded for whatever reason, one of the other instances will automatically take over.

Request de-duping

Since we now process from a single instance of the package it's easy to avoid fetching the same resource multiple times.

Caching

@amedia/user has a caching layer that easily lets us switch out the caching mechanism. We have two strategies:

  • SessionStorage: (to survive page loads) Used if available.
  • Global: Stores to a global variable on the current page.

Debugging

If you're curious about calls, response times, etc., there is a debug flag you can set on the URL debug=something. If you'd like it to survive page loads, you can add a key to localStorage or sessionStorage: localStorage.setItem('amedia-user:debug', 'something')