@amedia/user
Advanced tools
+114
| 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 }; |
+433
| 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 }; |
+29
-10
@@ -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" | ||
| } | ||
| } |
+42
-0
@@ -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 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
90856
28.68%7
40%657
238.66%527
8.66%2
Infinity%2
100%8
300%+ Added
+ Added
+ Added
+ Added