Skip to Content
URNAPI Reference

API reference

@evanion/urn 2.1.0 and its patch releases up to 2.1.1, the version on npm. Values on this page were produced by running the source at f9b1d93, a later commit than the 2.1.0 tag, and are not re-executed.

@evanion/urn exports one class, two functions, two errors and four types, one heading each below. It needs Node 20 or newer and ships ESM only, which require() loads on Node 20.19, 22.12 or newer.

URN

class

A URN, as defined by RFC 8141, with one deliberate deviation: RFC 8141 requires the literal scheme urn, and this class allows any scheme through the overridable URN.urn static.

Full documentation

The class conforms to RFC 8141 only under the default : separator. A subclass that overrides URN.separator checks the scheme and the NID against a character class that excludes the separator, with no length bounds, and is not RFC 8141 conformant. The NSS keeps the RFC grammar under every separator.

The optional r-, q- and f-components of RFC 8141 2.3 are split off the tail before the separator split, under every separator except one that itself contains ? or #. Such a separator cannot be told apart from a component introducer, so on that subclass parse throws an InvalidError with property: 'NSS' for any ? or # after the NID, and stringify throws one with property: 'COMPONENT' for any component.

Every static reads this, so they cannot be destructured the way JSON.stringify can: const { stringify } = URN then stringify('a') throws a TypeError, because the default parameter nid = this.nid runs with this undefined. Call them on the class.

URN is never instantiated. A subclass overrides the statics in the first three rows. parse, stringify and isValidFormat read all three off this. extractId, equals and sameNamespace read only separator, and belongsToNamespace reads separator and defaults its scheme to the class’s urn. GameURN in the examples on this page is URN with static override readonly nid = 'game'.

StaticDefaultWhat it is
urn'urn'the scheme this class writes and reads as its own
nid'nid'the namespace identifier this class writes and reads as its own
separator':'what the three parts are joined by and split on
schemeGrammarRFC 3986 schemea letter, then letters, digits, +, - and .
nidGrammarRFC 8141 NIDletters, digits and interior -, 2 to 32 characters, checked by stringify
nssGrammarRFC 8141 NSSletters, digits, -._~!$&'()*+,;=:@ and percent-encoded characters, plus / after the first character
rComponentGrammarpchar *(pchar / "/" / "?")the NSS set plus ? after the first character
qComponentGrammarpchar *(pchar / "/" / "?")the same production as the r-component
fComponentGrammarRFC 3986 fragmentthe NSS set plus ?, no first-character rule, and the only one to accept ''

'nid' is a placeholder, not a registered namespace, so every real namespace is foreign to the base URN class. Under a separator other than :, schemeGrammar and nidGrammar become a character class that excludes the separator, with no length bounds. parse accepts a one-character NID, which RFC 2141, the URN syntax RFC 8141 replaced, permitted and nidGrammar refuses:

..('baize'); // -> true ..('game'); // -> true ..('g'); // -> false .('urn:g:azul'); // -> true ..('brass-birmingham'); // -> true ..('brass birmingham'); // -> false

Passed as a bare callback, a static loses this. stringify, parse and extractId throw a TypeError. isValidFormat, equals and sameNamespace catch it and return false. belongsToNamespace throws only when its scheme argument is left out, and returns false under filter or map, which pass a third argument:

const = ['urn:game:azul', 'urn:game:hive']; .(() => .()); // -> ['azul', 'hive'] let = ''; try { .(.); } catch () { = ( as Error).; } ; // -> 'TypeError' .(.); // -> [] .(() => .()); // -> ['urn:game:azul', 'urn:game:hive']
export declare class { static readonly : string; static readonly : string; static readonly : string; static get (): RegExp; static get (): RegExp; static get (): RegExp; static get (): RegExp; static get (): RegExp; static get (): RegExp; protected static get (): RegExp; private static get (); private static get (); private static get (); static (: string): ParsedURN; static (: URNParts): string; static (: string, ?: string, ?: string): string; static (: string): boolean; static (: string): string; static (: string, : string): boolean; static (: string, : string, ?: string): boolean; static (: string, : string): boolean; private static ; private static ; private static ; private static ; private static ; private static ; private static ; private static ; private static ; private static ; }

What the tests state

102 behaviours

  • stringify
    • stringifies a basic URN
    • stringifies with a custom NID
    • rejects a scheme or NID carrying the separator
    • accepts an NSS carrying the separator
    • emits an NSS that starts with the NID verbatim
    • keeps a foreign namespace prefix verbatim in the NSS
    • accepts RFC 8141’s own example NSS
    • refuses a URN carrying an invalid character
    • refuses a NID carrying an invalid character
    • rejects a scheme that does not start with a letter
  • NID grammar
    • accepts lengths 2 through 32 on the write path
    • rejects a 1-character NID on the write path
    • rejects a 33-character NID on both paths
    • rejects a leading or trailing hyphen on both paths
    • rejects ’_’, ’.’ and ’~’ in a NID on both paths
    • accepts a 1-character NID on the read path only
    • exposes the write grammar through nidGrammar
  • NSS grammar
    • accepts the whole pchar set plus slash and percent-triplets
    • rejects a leading slash
    • rejects characters outside pchar
    • accepts the RFC 3986 sub-delims, colon and at-sign
    • rejects a malformed percent-triplet with a structural reason
    • neither encodes nor decodes on the write path
    • returns percent-triplets intact on the read path
  • scheme grammar
    • follows RFC 3986 under the default separator
  • round-tripping
    • round-trips the full pchar set through stringify and parse
    • round-trips an NSS carrying the separator
    • round-trips RFC 8141’s own example URNs unchanged
    • is not idempotent, and says so
  • class inheritance
    • derives from the URN class
    • derives from the URN class and sets the separator
    • derives from the URN class and sets the NID
  • custom separator
    • excludes the separator from the scheme and the NID
    • drops the RFC length bounds for the scheme and the NID
    • keeps the RFC pchar grammar for the NSS
    • round-trips through parse, extractId and isValidFormat
  • parse
    • parses a URN into its parts
    • retains a foreign NID in the NSS
    • retains a foreign scheme in the NSS, verbatim
    • distinguishes a foreign scheme from the class scheme
    • returns the original case
    • rejects character-invalid input
  • utility methods
    • isValidFormat · returns true for a valid URN
    • isValidFormat · returns false for an invalid URN
    • isValidFormat · agrees with parse on every input
    • extractId · extracts the identifier from a URN
    • extractId · extracts the identifier when the NSS carries a namespace
    • extractId · throws on character-invalid input
    • sameNamespace · returns true for URNs in the same namespace
    • sameNamespace · folds the case of the scheme and the NID
    • sameNamespace · returns false for URNs in different namespaces
    • sameNamespace · returns false for invalid URNs
    • belongsToNamespace · returns true when the URN belongs to the named namespace
    • belongsToNamespace · folds the case of the scheme and the NID
    • belongsToNamespace · returns false when the URN does not belong to the named namespace
    • belongsToNamespace · returns false for invalid URNs
    • equals · folds the case of the scheme and the NID
    • equals · compares the NSS byte for byte
    • equals · canonicalises percent-triplet hex digits to uppercase
    • equals · does not treat a percent-triplet as equal to the octet it encodes
    • equals · is not confused by a foreign NID the parse folded into the NSS
    • equals · returns false for malformed input rather than throwing
  • percent-encoding helpers
    • round-trips through decodeNss
  • malformed input
    • throws instead of leaking the string “undefined:” from parse
    • names the offending input in the parse error
    • throws a ValidationError, not a bare Error, from extractId
    • does not report two identical malformed strings as the same namespace
  • empty components
    • rejects an empty NSS rather than emitting an unparseable URN
    • rejects an empty NID and URN scheme
    • does not consider the empty string a valid component
  • subclass awareness
    • defaults belongsToNamespace to the subclass scheme
    • still honours an explicit expectedUrn
    • uses the subclass separator when retaining a foreign NID
    • does not let the statics be destructured
    • lets the unbound predicates answer false
  • r-, q- and f-components
    • parses a component-free URN to exactly three keys
    • splits each component off the NSS
    • ends the r-component at the first ’?=’
    • takes the f-component off before looking for a ’?’
    • keeps an empty f-component distinct from an absent one
    • keeps the components out of the NSS on a foreign namespace
    • emits the components from the object form of stringify
    • round-trips a URN with components through parse and stringify
    • rejects a component that breaks its grammar
    • rejects a bare ’?’ that introduces nothing
    • keeps the NSS argument of the positional form free of components
    • excludes the components from equivalence, per RFC 8141 3.1
    • drops the components from extractId
    • exposes the component grammars
    • parses components under a custom separator
    • does not parse components when the separator carries a delimiter
    • refuses a component tail when the separator carries a delimiter
  • the object form of stringify
    • accepts a parsed URN directly
    • defaults the scheme and the NID to the class
    • does not invert parse on a foreign namespace
    • agrees with the positional form
    • validates the object form the same way
    • round-trips through the object form, not the spread
  • error hierarchy
    • exposes InvalidError as a ValidationError
    • carries a structural reason when no single character is at fault
    • falls back to a generic message when given neither
    • lets a single catch handle both error kinds

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

import { , } from '@evanion/urn'; class extends { static override readonly = 'game'; } const = .('brass-birmingham'); ; // -> 'urn:game:brass-birmingham' let = ''; try { .('brass birmingham'); } catch () { if ( instanceof ) = .; } ; // -> "NSS contains invalid character ' ' in 'brass birmingham'"

URN.stringify

stringify(parts) and stringify(nss, nid?, urn?) write a URN string. nid and urn default to the calling class’s own statics. Both forms throw InvalidError when a part is empty or breaks its grammar:

.('azul'); // -> 'urn:game:azul' .('azul', 'game'); // -> 'urn:game:azul' .('azul', 'game', 'baize'); // -> 'baize:game:azul' .({ : 'azul', : 'game' }); // -> 'urn:game:azul' .(.('urn:game:azul')); // -> 'urn:game:azul'

The object form’s keys match what parse returns, and only the object form writes the r-, q- and f-components. The positional arguments run in the reverse order of parse’s return, so stringify(...Object.values(parse(x))) puts the scheme where the NSS belongs. stringify writes the NSS verbatim, so it neither deduplicates nor is idempotent:

.('game:azul'); // -> 'urn:game:game:azul' .('urn:game:game:azul').; // -> 'game:azul' .(.('azul')); // -> 'urn:game:urn:game:azul'

URN.parse

parse(urnString) returns a ParsedURN, with every part in the case it was written in and every percent-encoded character intact. A scheme or NID that differs from the class’s own, compared without regard to case, stays in the nss:

.('urn:game:azul'); // -> { urn: 'urn', nid: 'game', nss: 'azul' } .('URN:GAME:azul'); // -> { urn: 'URN', nid: 'GAME', nss: 'azul' } .('urn:order:order-2026-0042'); // -> { urn: 'urn', nid: 'order', nss: 'order:order-2026-0042' } .('baize:game:azul'); // -> { urn: 'baize', nid: 'game', nss: 'baize:game:azul' }

parse throws ValidationError for a string that is not a URN, and InvalidError for a part that breaks its grammar.

URN.isValidFormat

isValidFormat(urnString) returns true when parse would succeed. It calls parse, so the two cannot disagree, and it never throws:

function (: string) { return .() ? .() : null; } ('urn:game:hive'); // -> { urn: 'urn', nid: 'game', nss: 'hive' } ('urn:game:spirit island'); // -> null ('hive'); // -> null ('urn:order:order-2026-0042'); // -> { urn: 'urn', nid: 'order', nss: 'order:order-2026-0042' }

URN.extractId

extractId(urnString) returns everything after the scheme and the NID, with any r-, q- or f-component dropped. On a foreign namespace it differs from parse(urnString).nss, which keeps the NID. The base URN class’s own NID is 'nid', so game is foreign to it:

.('urn:game:azul').; // -> 'game:azul' .('urn:game:azul').; // -> 'game' .('urn:game:azul'); // -> 'azul'

extractId throws ValidationError on malformed input.

URN.sameNamespace

sameNamespace(a, b) returns true when both URNs have the same scheme and the same NID, compared case-folded. Malformed input returns false:

.('urn:game:azul', 'URN:Game:hive'); // -> true .('urn:game:azul', 'urn:order:order-2026-0042'); // -> false .('azul', 'azul'); // -> false

URN.belongsToNamespace

belongsToNamespace(urnString, nid, urn?) returns true when the URN is in the named namespace. urn defaults to the calling class’s own scheme. Malformed input returns false:

.('urn:game:azul', 'game'); // -> true .('baize:game:azul', 'game'); // -> false .('baize:game:azul', 'game', 'baize'); // -> true

URN.equals

equals(a, b) is RFC 8141 §3.1 equivalence. The scheme and the NID fold case. The NSS compares character for character, except that the hex digits of a percent-triplet fold to uppercase. A percent-encoded octet is never decoded, and the r-, q- and f-components are ignored. Malformed input returns false:

.('URN:GAME:azul', 'urn:game:azul'); // -> true .('urn:game:Brass%3a%20Birmingham', 'urn:game:Brass%3A%20Birmingham'); // -> true .('urn:game:Brass%3A%20Birmingham', 'urn:game:Brass:%20Birmingham'); // -> false .('urn:game:Azul', 'urn:game:azul'); // -> false

encodeNss

function

Percent-encodes an arbitrary string into a valid NSS.

Full documentation

Everything outside RFC 3986’s unreserved set is encoded as UTF-8 percent-triplets with uppercase hex, so the result is safe in any position of an NSS. Neither stringify nor parse calls this: encoding is an explicit, separate step, so a value can never be double-encoded by accident.

encodeNss(raw) leaves A-Z a-z 0-9 - . _ ~ alone and percent-encodes every other character, : included.

function (: string): string;

What the tests state

2 behaviours

  • encodeNss
    • encodes everything outside the unreserved set
    • produces an NSS the grammar accepts

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

import { , , } from '@evanion/urn'; .('urn:game:Brass: Birmingham'); // -> false const = ('Brass: Birmingham'); ; // -> 'Brass%3A%20Birmingham' const = .(, 'game'); ; // -> 'urn:game:Brass%3A%20Birmingham' (.()); // -> 'Brass: Birmingham'

decodeNss

function

Decodes the percent-triplets in an NSS back to the raw string.

throws {ValidationError} when the input contains a malformed or truncated percent sequence.

decodeNss(encoded) reverses encodeNss. It throws ValidationError on a % that is not followed by two hex digits, and on triplets that do not decode as UTF-8.

function (: string): string;

What the tests state

2 behaviours

  • decodeNss
    • decodes triplets the library itself never produces
    • throws a ValidationError on a malformed percent sequence

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

import { , } from '@evanion/urn'; ('Brass%3A%20Birmingham'); // -> 'Brass: Birmingham' let = ''; try { ('Brass%3'); } catch () { if ( instanceof ) = .; } ; // -> "Malformed percent-encoding in 'Brass%3': '%' must be followed by two hex digits."

ValidationError

error

Base class for every error this library throws. Catch it to handle any validation failure without naming the subclasses.

ValidationError is the base class of every error @evanion/urn throws. parse, extractId and decodeNss throw it directly for input that is not a URN or not a valid encoding, so catching it covers InvalidError too.

export declare class extends { constructor(: string); }
import { , , } from '@evanion/urn'; function (: string): string { try { .(); return 'none'; } catch () { if ( instanceof ) return `bad ${.}`; if ( instanceof ) return 'not a URN'; throw ; } } ('urn:game:azul'); // -> 'none' ('urn:game:spirit island'); // -> 'bad NSS' ('azul'); // -> 'not a URN'

InvalidError

error

Thrown when one part of a URN is empty, contains a character outside its role’s permitted set, or breaks a structural rule of its role’s grammar (a length bound, a leading hyphen, a truncated percent-triplet).

InvalidError extends ValidationError, and stringify and parse throw it for one part that is empty or breaks its grammar.

PropertyWhat it holds
propertythe part: 'URN' for the scheme, 'NID', 'NSS', 'R-COMPONENT', 'Q-COMPONENT', 'F-COMPONENT' or 'COMPONENT'
valuethe value of that part
invalidCharthe first character outside the part’s set, when one is at fault
reasonthe structural rule broken when every character is allowed, such as a length bound

'COMPONENT' is the property when a subclass whose separator contains ? or # is asked to write a component.

export declare class extends { constructor(: string, : string, ?: string, ?: string); private static ; readonly : string; readonly : string; readonly ?: string; readonly ?: string; }
import { , } from '@evanion/urn'; function (: string, : string): | undefined { try { .(, ); return ; } catch () { if ( instanceof ) return ; throw ; } } const = ('spirit island', 'game'); ?.; // -> 'NSS' ?.; // -> ' ' ?.; // -> "NSS contains invalid character ' ' in 'spirit island'" const = ('spirit-island', 'g'); ?.; // -> 'NID' ?.; // -> 'must be at least 2 characters long'

ParsedURN

interface

The object returned by URN.parse.

Full documentation

The parts are plain strings, and deliberately not type parameters. parse takes a runtime string, so it cannot know their literal types; free type parameters here would let URN.parse<'a', 'b', 'c'>(someString) assert a shape nothing verifies.

The three component keys are absent rather than undefined when the URN carries no components, so such a URN parses to exactly { urn, nid, nss }.

ParsedURN is what parse returns: urn, nid and nss, plus whichever components the URN carries.

interface ParsedURN extends URNComponents { : string; : string; : string; }
.('urn:game:brass-birmingham?=edition=2018#setup'); // -> { urn: 'urn', nid: 'game', nss: 'brass-birmingham', fComponent: 'setup', qComponent: 'edition=2018' }

URNParts

interface

The object form of URN.stringify’s arguments.

Full documentation

The keys match ParsedURN, so a parsed URN can be handed straight back: on one class, stringify(parse(x)) returns x for a URN in that class’s own namespace. The scheme and the NID are optional and fall back to the class’s own, which is what makes { nss } alone valid.

URNParts is what the object form of stringify takes. Only nss is required, and urn and nid fall back to the class’s own. A ParsedURN is assignable to it.

interface URNParts extends URNComponents { ?: string; ?: string; : string; }
import type { ParsedURN, URNParts } from '@evanion/urn'; class extends { static override readonly = 'game'; } const : ParsedURN = .('urn:game:azul#setup'); const : URNParts = { ..., : 'scoring' }; .(); // -> 'urn:game:azul#scoring' const : URNParts = { : 'hive' }; .(); // -> 'urn:game:hive'

URNComponents

interface

The three optional components RFC 8141 2.3 allows after the NSS.

Full documentation

Each is carried in its wire form, without its introducing delimiter and without any percent-decoding. RFC 8141 3.1 excludes all three from URN-equivalence, so URN.equals ignores them.

URNComponents holds rComponent, qComponent and fComponent, each optional. A key is absent when the URN carries no such component, and fComponent may be ''. r-, q- and f-components covers the syntax.

interface URNComponents { ?: string; ?: string; ?: string; }
function ({ , }: URNComponents): string { return [, ].().(' / '); } (.('urn:game:azul?=edition=2017#scoring')); // -> 'edition=2017 / scoring' 'qComponent' in .('urn:game:azul'); // -> false

IFullURN

type

A URN string with known parts, for annotating literals in consumer code.

Full documentation

Assumes the default : separator. A subclass with a custom separator cannot be described by this type.

IFullURN<URN, NID, NSS> is the template literal type `${URN}:${NID}:${NSS}`. stringify returns plain string, because a subclass may override the :.

export type < extends string, extends string, extends string> = `${}:${}:${}`;
import type { } from '@evanion/urn'; type = <'urn', 'game', string>; const : = 'urn:game:azul'; .(); // -> 'azul' // @ts-expect-error: an order URN is not a GameId const : = 'urn:order:order-2026-0042'; .(); // -> true
Last updated on