Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@amedia/user

Package Overview
Dependencies
Maintainers
108
Versions
273
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amedia/user - npm Package Compare versions

Comparing version
1.2.3
to
1.3.0
+114
user.node.d.ts
import { ServiceUser, LoginStatus, PrimarySite } from '@amedia/user-core';
export { AccessFeatures, AffiliationType, DEFAULT_CLIENT_ID, FetchTimeoutError, IgnoredNetworkError, LoginStatus, PrimarySite, ProblemJsonError, ResponseError, ServerError, ServiceUser, State, UserAttributes, ValidationError } from '@amedia/user-core';
import { JWTPayload } from 'jose';
interface AidClientOptions {
/** Absolute base URL for aID (e.g. "https://aid.example.no"). Required. */
baseUrl: string;
/** Optional additional headers to merge into every request. */
headers?: Record<string, string>;
}
interface AidClient {
/**
* Fetch the authenticated user's own profile.
*
* @param sessionId - Value of the user's `aid.session` cookie.
* @param filter - Fields to include in the response. Defaults to `["uuid", "name", "tracking_key", "access", "session_tracking_key"]`.
* @returns Partial `ServiceUser` object containing only the requested fields.
*/
getSelf: (sessionId: string, filter?: string[]) => Promise<ServiceUser>;
/**
* Fetch the access features granted to the authenticated user for a given site.
*
* @param sessionId - Value of the user's `aid.session` cookie.
* @param siteDomain - Full domain of the site to check access for (e.g. `"www.ba.no"`).
* @returns Array of access feature strings (e.g. `["plus", "sport"]`). Empty array means no access.
*/
getAccessFeatures: (sessionId: string, siteDomain: string) => Promise<string[]>;
/**
* Fetch the current login flow status for the user's session.
*
* @param sessionId - Value of the user's `aid.session` cookie.
* @returns `LoginStatus.INITIATED` | `LoginStatus.COMPLETED` | `LoginStatus.ERROR`.
*/
getLoginStatus: (sessionId: string) => Promise<LoginStatus>;
}
/**
* Create a server-side aID client bound to a given baseUrl.
*
* The returned client reuses the pure HTTP functions from @amedia/user-core —
* same URL paths, same schema validation — so the server surface mirrors
* what the browser does, forwarding the user's `aid.session` cookie value
* so aID identifies the request as coming from that user.
*/
declare const createAidClient: (options: AidClientOptions) => AidClient;
interface ShamoClientOptions {
baseUrl: string;
token: string;
headers?: Record<string, string>;
}
interface ShamoClient {
getPrimarySite: (accessFeatures: string[]) => Promise<PrimarySite | null>;
}
declare const createShamoClient: (options: ShamoClientOptions) => ShamoClient;
interface JwksOptions {
/** Maximum age of the JWKS cache in milliseconds. Default 10 minutes. */
cacheMaxAge?: number;
/** Timeout for the JWKS HTTP fetch in milliseconds. Default 5 seconds. */
timeoutDuration?: number;
/** Cooldown before re-fetching after a cache miss, in milliseconds. Default 30 seconds. */
cooldownDuration?: number;
}
/**
* Clears the JWKS resolver cache. Primarily for tests.
*/
declare const clearJwksCache: () => void;
interface ValidateTokenOptions {
/** JWKS endpoint URL. Required. */
jwksUrl: string;
/** Expected `iss` claim value (or array of acceptable values). */
issuer: string | string[];
/** Expected `aud` claim value (or array). */
audience: string | string[];
/**
* Allowed signing algorithms (explicit allow-list). Required — leaving
* this unset would let a forged JWT be verified under any algorithm the
* JWKS key happens to support, including unintended ones. Typical aID
* tokens use `["RS256"]`.
*/
algorithms: string[];
/** Clock skew tolerance in seconds. Default 5s. */
clockTolerance?: number;
/** JWKS cache/timeout overrides. */
jwks?: JwksOptions;
}
interface ValidatedToken {
/** Fully parsed and verified JWT payload. */
payload: JWTPayload;
/** Header fields from the JWT (algorithm used, kid, etc.). */
header: {
alg: string;
kid?: string;
typ?: string;
};
}
/**
* Verifies a JWT issued by aID:
* - signature (via JWKS)
* - expiry (`exp`)
* - not-before (`nbf`)
* - issuer (`iss`)
* - audience (`aud`)
* - algorithm allow-list
*
* On failure throws a {@link ValidationError} with a stable `code` describing
* the failure mode, plus a human-readable message. Consumers can catch and
* map to HTTP 401/403 as appropriate.
*/
declare const validateToken: (jwt: string, options: ValidateTokenOptions) => Promise<ValidatedToken>;
export { clearJwksCache, createAidClient, createShamoClient, validateToken };
export type { AidClient, AidClientOptions, JwksOptions, ShamoClient, ShamoClientOptions, ValidateTokenOptions, ValidatedToken };
import * as v from 'valibot';
import { createRemoteJWKSet, jwtVerify, errors } from 'jose';
function tryParseJSON(value) {
try {
return JSON.parse(value);
}
catch {
return null;
}
}
class ResponseError extends Error {
constructor(body, response, nestedException) {
const message = `Request failed (${response.status}/${response.statusText}: ${response.url || '[missing url]'}) Body: ${body || '[empty body received]'}`;
super(message + (nestedException ? `. Reason: ${nestedException.message}` : ''));
this.body = body;
this.response = response;
this.nestedException = nestedException;
}
}
class ProblemJson {
constructor(data) {
this.type = data.type;
this.title = data.title;
this.detail = data.detail;
this.status = data.status;
}
static unknown(body, status) {
return new ProblemJson({
type: 'about:blank',
title: 'Unparseable problem response',
detail: body,
status,
});
}
}
class ProblemJsonError extends ResponseError {
constructor(body, response) {
super(body, response);
const parsed = tryParseJSON(body);
this.problem = parsed
? new ProblemJson(parsed)
: ProblemJson.unknown(body, response.status);
}
}
class ValidationError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
}
class ServerError extends Error {
}
class FetchTimeoutError extends Error {
constructor(message, partialData) {
super(message);
this.partialData = partialData;
}
}
class PaywallError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
}
PaywallError.ERROR_RELOADED_NO_ACCESS = 'RELOADED_NO_ACCESS';
PaywallError.ERROR_ENABLING_ACCESS_FAILED = 'ENABLING_ACCESS_FAILED';
PaywallError.ERROR_NO_ACCESS_AFTER_SUCCESSFUL_ACTIVATION_WITH_REQUESTED_ACCESS_FEATURES = 'NO_ACCESS_AFTER_SUCCESSFUL_ACTIVATION_WITH_REQUESTED_ACCESS_FEATURES';
class IgnoredNetworkError extends Error {
constructor(originalError) {
super(originalError?.message);
this.originalError = originalError;
}
}
const AidNamespaceSchema = v.record(v.string(), v.string());
const AidStorageSchema = v.record(v.string(), AidNamespaceSchema);
v.record(v.string(), AidStorageSchema);
v.object({
errors: v.array(v.object({
code: v.string(),
detail: v.string(),
})),
});
const PrivacyPreferencesSchema = v.object({
allow_research_usage: v.boolean(),
personalized_content: v.boolean(),
});
const SiteAccessSchema = v.object({
site_domain: v.string(),
access_features: v.array(v.string()),
});
const ServiceUserSchema = v.partial(v.object({
uuid: v.string(),
name: v.string(),
tracking_key: v.string(),
avatar: v.string(),
access: v.array(SiteAccessSchema),
privacy_preferences: PrivacyPreferencesSchema,
session_tracking_key: v.string(),
}));
v.object({
type: v.string(),
title: v.string(),
detail: v.string(),
status: v.number(),
instance: v.optional(v.string()),
});
var LoginStatus;
(function (LoginStatus) {
LoginStatus["INITIATED"] = "INITIATED";
LoginStatus["COMPLETED"] = "COMPLETED";
LoginStatus["ERROR"] = "ERROR";
})(LoginStatus || (LoginStatus = {}));
const LoginStatusResponseSchema = v.object({
status: v.enum(LoginStatus),
});
var AffiliationType;
(function (AffiliationType) {
AffiliationType["ACCESS"] = "access";
AffiliationType["FAMILY"] = "family";
AffiliationType["SUBSCRIPTION"] = "subscription";
})(AffiliationType || (AffiliationType = {}));
const PrimarySiteSchema = v.object({
domain: v.string(),
name: v.string(),
affiliation: v.literal(AffiliationType.ACCESS),
affiliations: v.array(v.union([
v.literal(AffiliationType.ACCESS),
v.literal(AffiliationType.FAMILY),
v.literal(AffiliationType.SUBSCRIPTION),
])),
});
const isProblemJsonContentType = (contentType) => /^application\/problem\+json/.test(contentType ?? '');
function handleNetworkError(error) {
throw new IgnoredNetworkError(error);
}
async function unmarshalResponse(response) {
const body = await response.text();
if (body.length === 0) {
return null;
}
try {
const data = JSON.parse(body);
if (typeof data === 'undefined') {
return null;
}
return data;
}
catch (e) {
if (e instanceof SyntaxError) {
throw new ResponseError(body, response, e);
}
throw e;
}
}
async function failOnNonSuccess(response) {
if (response.ok) {
return response;
}
const body = await response.text();
if (isProblemJsonContentType(response.headers.get('Content-Type'))) {
throw new ProblemJsonError(body, response);
}
try {
const json = JSON.parse(body);
if (['message'].every((k) => Object.keys(json).includes(k)) &&
/Failed to fetch|Load Failed/i.test(json.message)) {
throw new IgnoredNetworkError(new Error(json.message));
}
}
catch (parseOrRethrow) {
if (parseOrRethrow instanceof IgnoredNetworkError)
throw parseOrRethrow;
if (/Failed to fetch|Load Failed/i.test(body)) {
throw new IgnoredNetworkError(new Error(body));
}
}
throw new ResponseError(body, response);
}
/**
* Pure HTTP primitive. Issues a fetch, maps non-2xx/network/problem+json
* to typed exceptions, and returns the JSON body (or null for empty).
*
* No retries, no emergency-mode handling, no circuit breaker, no logging —
* consumers layer those concerns on top.
*/
const coreFetcher = (url, options) => fetch(url, options)
.catch(handleNetworkError)
.then(failOnNonSuccess)
.then(unmarshalResponse);
/**
* Pure HTTP primitive + valibot validation. Throws ValiError from valibot
* on schema mismatch; callers are responsible for catching it if they want.
*/
const coreSchemaVerifiedFetch = (schema, url, options) => coreFetcher(url, options).then((json) => v.parse(schema, json));
const DEFAULT_CLIENT_ID = 'default';
const buildUrl$1 = (path, baseUrl) => baseUrl ? `${baseUrl.replace(/\/$/, '')}${path}` : path;
const resolveSchemaVerifiedFetch = (opts) => opts.schemaVerifiedFetch ?? coreSchemaVerifiedFetch;
/**
* GET /aid/api/users/self — returns ServiceUser schema.
*/
const FILTER_FIELD_RE = /^[a-zA-Z0-9_]+$/;
const fetchSelf = (filter = [
'uuid',
'name',
'tracking_key',
'access',
'session_tracking_key',
], opts = {}) => {
for (const field of filter) {
if (!FILTER_FIELD_RE.test(field)) {
throw new TypeError(`fetchSelf: filter field must match ${FILTER_FIELD_RE} (got "${field}")`);
}
}
const filterQuery = filter.length > 0 ? `?filter=(${filter.join(',')})` : '';
return resolveSchemaVerifiedFetch(opts)(ServiceUserSchema, buildUrl$1(`/aid/api/users/self${filterQuery}`, opts.baseUrl), { headers: opts.headers });
};
/**
* GET /aid/api/access — returns string[] of access features for a site.
*/
const fetchAccessFeatures = (siteDomain, opts = {}) => resolveSchemaVerifiedFetch(opts)(v.array(v.string()), buildUrl$1(`/aid/api/access?${new URLSearchParams({ site_domain: siteDomain })}`, opts.baseUrl), { headers: opts.headers });
/**
* GET /aid/login/status — login flow status enum.
*/
const fetchLoginStatus = (opts = {}) => resolveSchemaVerifiedFetch(opts)(LoginStatusResponseSchema, buildUrl$1(`/aid/login/status`, opts.baseUrl), { headers: opts.headers }).then((d) => d.status);
const buildUrl = (path, baseUrl) => baseUrl ? `${baseUrl.replace(/\/$/, '')}${path}` : path;
/**
* GET /api/shamo/v2/access/primary_site — returns primary site or null.
*/
const fetchPrimarySite = (accessFeatures, opts = {}) => {
const schemaVerifiedFetch = opts.schemaVerifiedFetch ?? coreSchemaVerifiedFetch;
return schemaVerifiedFetch(v.union([v.null(), PrimarySiteSchema]), buildUrl(`/api/shamo/v2/access/primary_site?${new URLSearchParams({
access_features: accessFeatures.join(','),
})}`, opts.baseUrl), { method: 'GET', headers: opts.headers });
};
/**
* Reject URLs that are not absolute http(s) — protects against SSRF via
* `file:`, `gopher:`, missing scheme (empty → relative), or malformed input
* when a URL is built from configuration / env / request context.
*/
const assertHttpUrl = (value, fieldName) => {
if (typeof value !== 'string' || value.length === 0) {
throw new TypeError(`${fieldName} must be a non-empty string`);
}
let parsed;
try {
parsed = new URL(value);
}
catch {
throw new TypeError(`${fieldName} must be an absolute URL: ${value}`);
}
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
throw new TypeError(`${fieldName} must use http or https (got ${parsed.protocol})`);
}
return value;
};
/**
* Reject header values containing CR, LF or NUL so a caller cannot inject
* extra HTTP headers / cookies by passing a crafted session id or token.
* Throws TypeError on bad input; returns the value unchanged otherwise.
*/
const assertHeaderSafe = (value, fieldName) => {
if (typeof value !== 'string' || value.length === 0) {
throw new TypeError(`${fieldName} must be a non-empty string`);
}
if (/[\r\n\0]/.test(value)) {
throw new TypeError(`${fieldName} must not contain CR/LF/NUL characters`);
}
return value;
};
const optsWith$1 = (c, sessionId) => ({
baseUrl: c.baseUrl,
// Caller headers spread FIRST so they cannot override the session Cookie
// we append last.
headers: {
...c.headers,
Cookie: `aid.session=${assertHeaderSafe(sessionId, 'sessionId')}`,
},
});
/**
* Create a server-side aID client bound to a given baseUrl.
*
* The returned client reuses the pure HTTP functions from @amedia/user-core —
* same URL paths, same schema validation — so the server surface mirrors
* what the browser does, forwarding the user's `aid.session` cookie value
* so aID identifies the request as coming from that user.
*/
const createAidClient = (options) => {
assertHttpUrl(options.baseUrl, 'baseUrl');
return {
getSelf: (sessionId, filter) => fetchSelf(filter, optsWith$1(options, sessionId)),
getAccessFeatures: (sessionId, siteDomain) => fetchAccessFeatures(siteDomain, optsWith$1(options, sessionId)),
getLoginStatus: (sessionId) => fetchLoginStatus(optsWith$1(options, sessionId)),
};
};
const optsWith = (c) => ({
baseUrl: c.baseUrl,
// Caller headers spread FIRST so they cannot override the Authorization
// header we append last.
headers: {
...c.headers,
Authorization: `Bearer ${assertHeaderSafe(c.token, 'token')}`,
},
});
const createShamoClient = (options) => {
assertHttpUrl(options.baseUrl, 'baseUrl');
return {
getPrimarySite: (accessFeatures) => fetchPrimarySite(accessFeatures, optsWith(options)),
};
};
/**
* Cache of JWKS resolvers keyed by URL. `createRemoteJWKSet` already caches
* the fetched JWKS in memory — we just avoid constructing multiple resolvers
* for the same URL, and allow TTL overrides.
*/
const cache = new Map();
const getJwks = (jwksUrl, options = {}) => {
assertHttpUrl(jwksUrl, 'jwksUrl');
const existing = cache.get(jwksUrl);
if (existing)
return existing;
const resolver = createRemoteJWKSet(new URL(jwksUrl), {
cacheMaxAge: options.cacheMaxAge ?? 10 * 60 * 1000,
timeoutDuration: options.timeoutDuration ?? 5000,
cooldownDuration: options.cooldownDuration ?? 30000,
});
cache.set(jwksUrl, resolver);
return resolver;
};
/**
* Clears the JWKS resolver cache. Primarily for tests.
*/
const clearJwksCache = () => {
cache.clear();
};
// TBD resolved with aID team on 2026-04-21:
// - JWKS URL: — resolve before production use; see ValidateTokenOptions.jwksUrl
// - Issuer: — resolve before production use; see ValidateTokenOptions.issuer
// - Audience: — resolve before production use; see ValidateTokenOptions.audience (may be per-consumer)
// - Algorithms: — resolve before production use; see ValidateTokenOptions.algorithms (e.g. ['RS256'])
// - JWKS TTL: — resolve before production use; see ValidateTokenOptions.jwks.cacheMaxAge
/**
* Verifies a JWT issued by aID:
* - signature (via JWKS)
* - expiry (`exp`)
* - not-before (`nbf`)
* - issuer (`iss`)
* - audience (`aud`)
* - algorithm allow-list
*
* On failure throws a {@link ValidationError} with a stable `code` describing
* the failure mode, plus a human-readable message. Consumers can catch and
* map to HTTP 401/403 as appropriate.
*/
const validateToken = async (jwt, options) => {
const keySet = getJwks(options.jwksUrl, options.jwks);
return validateTokenWithKeySet(jwt, keySet, options);
};
/**
* Internal test seam. Accepts a pre-constructed JWKS resolver (either
* remote via `createRemoteJWKSet` or local via `createLocalJWKSet`) so tests
* can bypass HTTP. Exported from the module but NOT from the package index.
*/
const validateTokenWithKeySet = async (jwt, keySet, options) => {
if (!Array.isArray(options.algorithms) || options.algorithms.length === 0) {
throw new ValidationError('config_invalid', 'algorithms must be a non-empty array of allowed signing algorithms');
}
try {
const { payload, protectedHeader } = await jwtVerify(jwt, keySet, {
issuer: options.issuer,
audience: options.audience,
algorithms: options.algorithms,
clockTolerance: options.clockTolerance ?? 5,
});
return {
payload,
header: {
alg: protectedHeader.alg,
kid: protectedHeader.kid,
typ: protectedHeader.typ,
},
};
}
catch (err) {
throw mapJoseError(err);
}
};
function mapJoseError(err) {
if (err instanceof errors.JWTExpired) {
return new ValidationError('token_expired', err.message);
}
if (err instanceof errors.JWTClaimValidationFailed) {
return new ValidationError(`claim_invalid:${err.claim ?? 'unknown'}`, err.message);
}
if (err instanceof errors.JWSSignatureVerificationFailed) {
return new ValidationError('signature_invalid', err.message);
}
if (err instanceof errors.JWTInvalid ||
err instanceof errors.JWSInvalid) {
return new ValidationError('token_malformed', err.message);
}
if (err instanceof errors.JWKSNoMatchingKey) {
return new ValidationError('unknown_kid', err.message);
}
if (err instanceof errors.JOSEError) {
return new ValidationError(`jose_${err.code ?? 'error'}`, err.message);
}
if (err instanceof Error) {
return new ValidationError('validation_failed', err.message);
}
return new ValidationError('validation_failed', String(err));
}
export { AffiliationType, DEFAULT_CLIENT_ID, FetchTimeoutError, IgnoredNetworkError, LoginStatus, ProblemJsonError, ResponseError, ServerError, ValidationError, clearJwksCache, createAidClient, createShamoClient, validateToken };
//# sourceMappingURL=user.node.js.map
+6
-0
# @amedia/user
## 1.3.0
### Minor Changes
- [#1230](https://github.com/amedia/amedia-user-js/pull/1230) [`c9819f4`](https://github.com/amedia/amedia-user-js/commit/c9819f480437f63fd1508f4d9d6deafcf56475e1) Thanks [@csandven](https://github.com/csandven)! - Add Node.js support. `@amedia/user` now publishes a node-specific bundle via `exports` conditions. Node consumers (`npm install @amedia/user`) get `createAidClient`, `createShamoClient`, `validateToken`, and shared types/exceptions. Browser consumers using the importmap/CDN flow are unaffected.
## 1.2.3

@@ -4,0 +10,0 @@

+5
-70

@@ -1,38 +0,4 @@

import * as v from 'valibot';
import { UserAttributes, ClientId, State, PrimarySite } from '@amedia/user-core';
export { FetchTimeoutError, PaywallError, PrivacyPreferences, State, UserAttributes, ValidationError } from '@amedia/user-core';
type AccessFeatures = string[];
interface UserAttributes {
uuid: string | null;
name: string | null;
trackingKey: string | null;
avatar: string | null;
/** @deprecated Always returns empty. Use data from [@amedia/user-datapoints](https://github.com/amedia/amedia-user-datapoints) instead. */
extraData: {
[index: string]: unknown;
};
access: AccessFeatures;
/** @deprecated Returns false for all keys. CMP data is now used. */
privacyPreferences: PrivacyPreferences | null;
sessionTrackingKey: string | null;
}
interface State {
isLoggedIn: boolean;
isCircuitBreakerTripped?: boolean;
emergencyMode?: string[];
}
/**
* @deprecated This is set to false as default. CMP data is now used.
*/
declare class PrivacyPreferences {
readonly allowResearchUsage: boolean;
readonly personalizedContent: boolean;
constructor(data: {
allowResearchUsage: boolean;
personalizedContent: boolean;
});
static default(): PrivacyPreferences;
}
type ClientId = string;
type NamespaceList = Array<string>;

@@ -84,15 +50,2 @@

declare enum AffiliationType {
ACCESS = "access",
FAMILY = "family",
SUBSCRIPTION = "subscription"
}
declare const PrimarySiteSchema: v.ObjectSchema<{
readonly domain: v.StringSchema<undefined>;
readonly name: v.StringSchema<undefined>;
readonly affiliation: v.LiteralSchema<AffiliationType.ACCESS, undefined>;
readonly affiliations: v.ArraySchema<v.UnionSchema<[v.LiteralSchema<AffiliationType.ACCESS, undefined>, v.LiteralSchema<AffiliationType.FAMILY, undefined>, v.LiteralSchema<AffiliationType.SUBSCRIPTION, undefined>], undefined>, undefined>;
}, undefined>;
type PrimarySite = v.InferOutput<typeof PrimarySiteSchema>;
declare class SiteAccessResponse {

@@ -140,15 +93,2 @@ readonly isLoggedIn: boolean;

declare class FetchTimeoutError extends Error {
partialData: Record<string, unknown>;
constructor(message: string, partialData: Record<string, unknown>);
}
declare class PaywallError extends Error {
code: string;
static ERROR_RELOADED_NO_ACCESS: string;
static ERROR_ENABLING_ACCESS_FAILED: string;
static ERROR_NO_ACCESS_AFTER_SUCCESSFUL_ACTIVATION_WITH_REQUESTED_ACCESS_FEATURES: string;
constructor(message: string, code: string);
}
declare class EmergencyModeError extends Error {

@@ -159,7 +99,2 @@ activeEmergencyModes: string[];

declare class ValidationError extends Error {
readonly code: string;
constructor(code: string, message: string);
}
declare class TimeoutError extends Error {

@@ -177,3 +112,3 @@ }

*/
declare function pollForAccess(requiredAccessFeatures?: string[], timeout_millis?: number): Promise<true>;
declare const pollForAccess: (requiredAccessFeatures?: string[], timeout_millis?: number) => Promise<true>;

@@ -203,3 +138,3 @@ type LoginPageParams = {

export { EmergencyModeError, FetchTimeoutError, PaywallError, PaywallUnlockRequest, PrivacyPreferences, SiteAccessRequest, SiteAccessResponse, TimeoutError, UserDataRequest, ValidationError, aidUrls, getLoginUrl, goToLoginPage, logout, pollForAccess, requestDataRefresh };
export type { PaywallUnlockResponse, State, UserAttributes };
export { EmergencyModeError, PaywallUnlockRequest, SiteAccessRequest, SiteAccessResponse, TimeoutError, UserDataRequest, aidUrls, getLoginUrl, goToLoginPage, logout, pollForAccess, requestDataRefresh };
export type { PaywallUnlockResponse };

@@ -9,21 +9,39 @@ {

"author": "Amedia Produkt og Teknologi AS (https://amedia.no)",
"version": "1.2.3",
"version": "1.3.0",
"type": "module",
"types": "./index.d.ts",
"publishConfig": {
"access": "public"
},
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js",
"default": "./index.js"
"browser": {
"types": "./index.d.ts",
"import": "./index.js"
},
"node": {
"types": "./user.node.d.ts",
"import": "./user.node.js"
},
"default": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"./node": {
"types": "./user.node.d.ts",
"import": "./user.node.js"
}
},
"publishConfig": {
"access": "public"
},
"files": [
"index.js",
"index.d.ts",
"user.node.js",
"user.node.d.ts",
"README.md",
"CHANGELOG.md"
],
"dependencies": {
"jose": "5.9.6",
"valibot": "1.3.1"
},
"eik": {

@@ -38,5 +56,6 @@ "server": "https://assets.acdn.no",

"scripts": {
"prepublishOnly": "(cd ../../ && npm run build) && cp ../../dist/user.d.ts index.d.ts",
"postpublish": "npx @eik/cli publish && rm index.d.ts"
"prepublishOnly": "cd ../../ && npm run build",
"prepack": "cp ../../dist/user.d.ts index.d.ts && cp ../../dist/user.node.js user.node.js && cp ../../dist/user.node.d.ts user.node.d.ts",
"postpublish": "npx @eik/cli publish && rm index.d.ts user.node.js user.node.d.ts"
}
}

@@ -25,2 +25,44 @@ # @amedia/user

## Node.js usage
`@amedia/user` ships a Node bundle in addition to the browser one. Install via npm and import as usual:
```bash
npm install @amedia/user
```
```ts
import { validateToken, createAidClient, ValidationError } from '@amedia/user';
// Verify a JWT issued by aID
try {
const { payload } = await validateToken(jwt, {
jwksUrl: 'https://...',
issuer: 'https://aid.example.no',
audience: 'api://your-service',
algorithms: ['RS256'],
});
// `payload.sub`, `payload.iat`, etc. are trusted
} catch (err) {
if (err instanceof ValidationError) {
// err.code is one of: token_expired, signature_invalid, claim_invalid:*, ...
}
}
// Call aID from the server
const aid = createAidClient({
baseUrl: 'https://aid.example.no',
token: '<bearer token>',
});
const user = await aid.getSelf();
```
Available node exports:
- `validateToken` — JWT verification with JWKS caching.
- `createAidClient({ baseUrl, token })` — server-side aID HTTP client.
- `createShamoClient({ baseUrl, token })` — server-side shamo HTTP client.
- Shared types: `UserAttributes`, `ServiceUser`, `LoginStatus`, `PrimarySite`, etc.
- Shared exceptions: `ProblemJsonError`, `ResponseError`, `ValidationError`, ...
## Importing the module

@@ -27,0 +69,0 @@