
Research
/Security News
Mini Shai-Hulud Campaign Hits Red Hat Cloud Services npm Packages
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.
@amedia/user
Advanced tools
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.
Please be advised that you are required to use ESM to use this 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.
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.
| 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 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> };
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 };
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 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.) |
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"] |
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.
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.
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.
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.
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:
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?
}
});
getLoginUrl({requestedUrl: string context: string}): stringBuilds and return the aID login URL. See goToLoginPage for details
goToLoginPage({requestedUrl: string context: string}): voidSend 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(): voidIf 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.
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 pageredirectOnAccess(requestedUrl: string): If the paywall unlocker finds sufficient access for the user, it will send
the user to requestedUrl. (Provides more flexibility than reloadOnAccess())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:
reloadOnAccess() or redirectOnAccess().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.
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:
withAccessFeatures-requirements.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.
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.
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.
There are a few techniques used to avoid over-fetching and fetching multiple times when once would do.
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.
Since we now process from a single instance of the package it's easy to avoid fetching the same resource multiple times.
@amedia/user has a caching layer that easily lets us switch out the caching mechanism. We have two strategies:
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')
FAQs
Client lib for working with aID user and associated data
The npm package @amedia/user receives a total of 456 weekly downloads. As such, @amedia/user popularity was classified as not popular.
We found that @amedia/user demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 114 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.

Research
/Security News
The North Korean malware loader hides in a Packagist-listed package and its GitHub branch to fetch and execute remote code in a likely Contagious Interview-style lure.

Security News
The Rust project is moving toward formal rules on LLM use in contributions after months of internal debate over maintainer burden, code quality, and contributor experience.